优化: miniapp渲染(图片/checkbox/代码高亮) + MCP规则共享下沉 + 文档重写 + 节点风格对齐
This commit is contained in:
+190
-12
@@ -157,7 +157,7 @@ devflow/
|
|||||||
├── crates/
|
├── crates/
|
||||||
│ ├── df-types/ # 核心类型、错误、常量、事件、augmentation
|
│ ├── df-types/ # 核心类型、错误、常量、事件、augmentation
|
||||||
│ ├── df-workflow/ # 工作流 DAG 引擎 (核心)
|
│ ├── df-workflow/ # 工作流 DAG 引擎 (核心)
|
||||||
│ ├── df-nodes/ # 内置节点集合 (AI/Script/Human)
|
│ ├── df-nodes/ # 内置节点集合 (AI/Script/Human/Git/HTTP/Notify/Subflow/Docker/自审/推进)
|
||||||
│ ├── df-ai/ # AI 编排层 (Multi-Provider/Coordinator)
|
│ ├── df-ai/ # AI 编排层 (Multi-Provider/Coordinator)
|
||||||
│ ├── df-execute/ # 执行运行时 (Shell)
|
│ ├── df-execute/ # 执行运行时 (Shell)
|
||||||
│ ├── df-storage/ # 存储层 (SQLite)
|
│ ├── df-storage/ # 存储层 (SQLite)
|
||||||
@@ -330,23 +330,25 @@ pub struct Decision {
|
|||||||
| 节点 | 功能 | 状态 |
|
| 节点 | 功能 | 状态 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| AINode | 调用 LLM,支持流式输出、工具调用 | ✅ 已实现 |
|
| AINode | 调用 LLM,支持流式输出、工具调用 | ✅ 已实现 |
|
||||||
|
| AiSelfReviewNode | AI 对任务产出自审(verdict 闸门) | ✅ 已实现 |
|
||||||
| ScriptNode | Shell/脚本执行 | ✅ 已实现 |
|
| ScriptNode | Shell/脚本执行 | ✅ 已实现 |
|
||||||
| HumanNode | 人工审批/确认 (阻塞) | ✅ 已实现 |
|
| HumanNode | 人工审批/确认 (阻塞) | ✅ 已实现 |
|
||||||
|
| TaskAdvanceNode | 任务状态推进链唯一 status 写入路径 | ✅ 已实现 |
|
||||||
| GitNode | Git 操作(branch/checkout/commit/merge/push/status/log) | ✅ 已实现 |
|
| GitNode | Git 操作(branch/checkout/commit/merge/push/status/log) | ✅ 已实现 |
|
||||||
| HTTPNode | HTTP 请求(GET/POST/PUT/DELETE) | ✅ 已实现 |
|
| HTTPNode | HTTP 请求(GET/POST/PUT/DELETE) | ✅ 已实现 |
|
||||||
| NotifyNode | 通知(桌面/Webhook) | ✅ 已实现 |
|
| NotifyNode | 通知(桌面/Webhook) | ✅ 已实现 |
|
||||||
| SubflowNode | 嵌套子工作流(深度限制防递归) | ✅ 已实现 |
|
| SubflowNode | 嵌套子工作流(深度限制防递归) | ✅ 已实现 |
|
||||||
| DockerNode | Docker 容器内构建/测试(环境检测+授权) | ✅ 已实现 |
|
| DockerNode | Docker 容器内构建/测试(环境检测+授权) | ✅ 已实现 |
|
||||||
|
|
||||||
> `crates/df-nodes/src/` 含 ai_node / script_node / human_node / git_node / http_node / notify_node / subflow_node / docker_node 8 文件(全部已实现)。
|
> `crates/df-nodes/src/` 含 10 个 `impl Node`(ai/ai_self_review/script/human/task_advance/git/http/notify/subflow/docker,全部已实现)。
|
||||||
|
|
||||||
## 六、数据模型
|
## 六、数据模型
|
||||||
|
|
||||||
### SQLite 表结构
|
### SQLite 表结构
|
||||||
|
|
||||||
> 真相源:`crates/df-storage/src/migrations.rs`(V1-V13,幂等迁移)。下表对齐真实 schema,字段/类型/默认值/外键与迁移文件一致。Rust 结构体见 `crates/df-storage/src/models.rs`。
|
> 真相源:`crates/df-storage/src/migrations.rs`(V1-V41,幂等迁移)。下表对齐真实 schema,字段/类型/默认值/外键与迁移文件一致。Rust 结构体见 `crates/df-storage/src/models.rs`。
|
||||||
|
|
||||||
共 **13 张业务表**(另含内部表 `schema_version` 记录迁移版本)。
|
共 **24 张业务表**(另含内部表 `schema_version` 记录迁移版本)。
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- 想法池 (V1 建表 / V2 补 promoted_to + ai_analysis + scores)
|
-- 想法池 (V1 建表 / V2 补 promoted_to + ai_analysis + scores)
|
||||||
@@ -362,6 +364,8 @@ CREATE TABLE ideas (
|
|||||||
promoted_to TEXT, -- 晋升后的 project_id (V2)
|
promoted_to TEXT, -- 晋升后的 project_id (V2)
|
||||||
ai_analysis TEXT, -- AI 分析结果 JSON (V2)
|
ai_analysis TEXT, -- AI 分析结果 JSON (V2)
|
||||||
scores TEXT, -- 多维评分 JSON: feasibility/impact/urgency/overall (V2)
|
scores TEXT, -- 多维评分 JSON: feasibility/impact/urgency/overall (V2)
|
||||||
|
related_ids TEXT, -- 关联灵感 ID JSON 数组 (V24)
|
||||||
|
deleted_at TEXT, -- 软删回收站 (V28)
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
@@ -380,18 +384,27 @@ CREATE TABLE projects (
|
|||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- 任务 (V1 建表 / V2 补 workflow_def_id + base_branch)
|
-- 任务 (V1 建表 / V2 补 workflow_def_id + base_branch / V20 补 idea_id / V14 补 deleted_at /
|
||||||
|
-- V15 补 review_rounds / V17 补 output_json / V29 补 queue+parent_id+content_json / V41 补 module_id)
|
||||||
CREATE TABLE tasks (
|
CREATE TABLE tasks (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
project_id TEXT NOT NULL REFERENCES projects(id),
|
project_id TEXT NOT NULL REFERENCES projects(id),
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
description TEXT NOT NULL DEFAULT '',
|
description TEXT NOT NULL DEFAULT '',
|
||||||
status TEXT NOT NULL DEFAULT 'todo', -- todo/in_progress/in_review/testing/done/blocked/cancelled
|
status TEXT NOT NULL DEFAULT 'todo', -- todo/in_progress/in_review/testing/done/blocked/cancelled
|
||||||
priority INTEGER NOT NULL DEFAULT 1,
|
priority INTEGER NOT NULL DEFAULT 2, -- 0=critical..3=low,默认 2=medium
|
||||||
branch_name TEXT,
|
branch_name TEXT,
|
||||||
assignee TEXT,
|
assignee TEXT,
|
||||||
workflow_def_id TEXT, -- 关联的工作流定义 ID (V2)
|
workflow_def_id TEXT, -- 关联的工作流定义 ID (V2)
|
||||||
base_branch TEXT, -- 基础分支 (V2)
|
base_branch TEXT, -- 基础分支 (V2)
|
||||||
|
idea_id TEXT REFERENCES ideas(id), -- 关联灵感 (V20)
|
||||||
|
review_rounds INTEGER NOT NULL DEFAULT 0, -- review 退回累计轮数 (V15)
|
||||||
|
output_json TEXT, -- AiNode 自审闭环产出 (V17)
|
||||||
|
queue TEXT NOT NULL DEFAULT 'todo', -- 管理池 backlog/todo/decision/active/done (V29)
|
||||||
|
parent_id TEXT REFERENCES tasks(id), -- 父任务(限 1 级嵌套) (V29)
|
||||||
|
content_json TEXT, -- 结构化需求规格 JSON (V29)
|
||||||
|
module_id TEXT REFERENCES project_modules(id) ON DELETE SET NULL, -- 关联工程 (V41)
|
||||||
|
deleted_at TEXT, -- 软删回收站 (V14)
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
@@ -432,7 +445,8 @@ CREATE TABLE workflow_executions (
|
|||||||
project_id TEXT, -- 关联项目 ID (V2)
|
project_id TEXT, -- 关联项目 ID (V2)
|
||||||
task_id TEXT, -- 关联任务 ID (V2)
|
task_id TEXT, -- 关联任务 ID (V2)
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
completed_at TEXT
|
completed_at TEXT,
|
||||||
|
updated_at TEXT -- 更新时间戳,排序/增量同步 (V33)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- 节点执行 (V1 建表) — 工作流执行过程中每个节点的运行快照
|
-- 节点执行 (V1 建表) — 工作流执行过程中每个节点的运行快照
|
||||||
@@ -449,7 +463,7 @@ CREATE TABLE node_executions (
|
|||||||
completed_at TEXT
|
completed_at TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
-- AI 提供商配置 (V9 建表) — 多 Provider 统一抽象
|
-- AI 提供商配置 (V9 建表 / V18 补 model_configs / V19 补 enabled + weight)
|
||||||
CREATE TABLE ai_providers (
|
CREATE TABLE ai_providers (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
@@ -460,29 +474,38 @@ CREATE TABLE ai_providers (
|
|||||||
models TEXT, -- JSON array of model names
|
models TEXT, -- JSON array of model names
|
||||||
is_default INTEGER NOT NULL DEFAULT 0,
|
is_default INTEGER NOT NULL DEFAULT 0,
|
||||||
config TEXT, -- JSON extra config
|
config TEXT, -- JSON extra config
|
||||||
|
model_configs TEXT, -- 模型能力配置 JSON (V18)
|
||||||
|
enabled INTEGER NOT NULL DEFAULT 1, -- 负载均衡池开关 (V19)
|
||||||
|
weight INTEGER NOT NULL DEFAULT 50, -- 负载均衡权重 (V19)
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- AI 对话 (V3 建表 / V4 补 archived / V5 补 prompt_tokens + completion_tokens / V6 补 models)
|
-- AI 对话 (V3 建表 / V4 补 archived / V5 补 prompt_tokens + completion_tokens / V6 补 models /
|
||||||
|
-- V16 补 pinned / V32 补 pinned_goals / V33 补 pending_approvals)
|
||||||
CREATE TABLE ai_conversations (
|
CREATE TABLE ai_conversations (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
title TEXT,
|
title TEXT,
|
||||||
messages TEXT NOT NULL DEFAULT '[]', -- JSON array of ChatMessage
|
messages TEXT NOT NULL DEFAULT '[]', -- JSON array of ChatMessage (V21 起消息拆分到 ai_messages)
|
||||||
provider_id TEXT,
|
provider_id TEXT,
|
||||||
model TEXT,
|
model TEXT,
|
||||||
models TEXT, -- 用过的所有 model JSON 数组字符串 (去重, V6)
|
models TEXT, -- 用过的所有 model JSON 数组字符串 (去重, V6)
|
||||||
archived INTEGER NOT NULL DEFAULT 0, -- 是否归档(侧栏折叠展示, V4)
|
archived INTEGER NOT NULL DEFAULT 0, -- 是否归档(侧栏折叠展示, V4)
|
||||||
|
pinned INTEGER NOT NULL DEFAULT 0, -- 侧栏置顶 (V16)
|
||||||
prompt_tokens INTEGER, -- 输入 token 累计 (流式 usage 落库, V5)
|
prompt_tokens INTEGER, -- 输入 token 累计 (流式 usage 落库, V5)
|
||||||
completion_tokens INTEGER, -- 输出 token 累计 (流式 usage 落库, V5)
|
completion_tokens INTEGER, -- 输出 token 累计 (流式 usage 落库, V5)
|
||||||
|
pinned_goals TEXT DEFAULT '[]', -- 目标钉扎持久化 (V32)
|
||||||
|
pending_approvals TEXT DEFAULT '{}', -- 挂起审批快照,重启恢复 (V33)
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- AI 工具执行审计 (V9 建表) — Agent 工具调用的审批/执行留痕
|
-- AI 工具执行审计 (V9 建表 / V21 补 message_id / V27 状态统一 executed→completed / V36 补 subtask_id)
|
||||||
|
-- Agent 工具调用的审批/执行留痕
|
||||||
CREATE TABLE ai_tool_executions (
|
CREATE TABLE ai_tool_executions (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
conversation_id TEXT,
|
conversation_id TEXT,
|
||||||
|
message_id TEXT, -- 关联消息 (V21)
|
||||||
tool_call_id TEXT NOT NULL,
|
tool_call_id TEXT NOT NULL,
|
||||||
tool_name TEXT NOT NULL,
|
tool_name TEXT NOT NULL,
|
||||||
arguments TEXT NOT NULL,
|
arguments TEXT NOT NULL,
|
||||||
@@ -491,7 +514,8 @@ CREATE TABLE ai_tool_executions (
|
|||||||
risk_level TEXT NOT NULL DEFAULT 'medium', -- low/medium/high
|
risk_level TEXT NOT NULL DEFAULT 'medium', -- low/medium/high
|
||||||
requested_at TEXT NOT NULL,
|
requested_at TEXT NOT NULL,
|
||||||
executed_at TEXT,
|
executed_at TEXT,
|
||||||
decided_by TEXT -- human/auto
|
decided_by TEXT, -- human/auto
|
||||||
|
subtask_id TEXT -- 归属子任务 (V36)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- 知识条目 (V7 建表 / V8 补 embedding / V10 补 reasoning) — 经验沉淀基本单元, 共享记忆层
|
-- 知识条目 (V7 建表 / V8 补 embedding / V10 补 reasoning) — 经验沉淀基本单元, 共享记忆层
|
||||||
@@ -510,6 +534,7 @@ CREATE TABLE knowledges (
|
|||||||
source_ref TEXT, -- 来源实体引用(如 conv:{id})
|
source_ref TEXT, -- 来源实体引用(如 conv:{id})
|
||||||
reasoning TEXT, -- AI 提炼判断依据("为何值得沉淀"), 手动录入为 NULL (V10)
|
reasoning TEXT, -- AI 提炼判断依据("为何值得沉淀"), 手动录入为 NULL (V10)
|
||||||
embedding BLOB, -- Vec<f32> 小端字节序列化, NULL=未嵌入走 LIKE 降级 (V8)
|
embedding BLOB, -- Vec<f32> 小端字节序列化, NULL=未嵌入走 LIKE 降级 (V8)
|
||||||
|
embedding_status TEXT, -- 嵌入状态,失败可补偿重试 (V23)
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
@@ -531,6 +556,159 @@ CREATE TABLE app_settings (
|
|||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- AI 消息 (V9 新库建表 / V21 老库补建 / V36 补 subtask_id / V38 补 token 列 / V39 补 cache/reason 列)
|
||||||
|
-- V21 起 ChatMessage 逐条拆行存储,支撑消息级 token / 分页 / 溯源
|
||||||
|
CREATE TABLE ai_messages (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
conversation_id TEXT NOT NULL,
|
||||||
|
seq INTEGER NOT NULL,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL DEFAULT '',
|
||||||
|
parts TEXT,
|
||||||
|
tool_call_id TEXT,
|
||||||
|
tool_calls TEXT,
|
||||||
|
model TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'active',
|
||||||
|
reasoning_content TEXT,
|
||||||
|
timestamp INTEGER,
|
||||||
|
subtask_id TEXT, -- 归属子任务 (V36)
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
prompt_tokens INTEGER, -- 消息级输入 token (V38)
|
||||||
|
completion_tokens INTEGER, -- 消息级输出 token (V38)
|
||||||
|
prompt_cache_hit_tokens INTEGER, -- 缓存命中 token (V39)
|
||||||
|
prompt_cache_miss_tokens INTEGER, -- 缓存未命中 token (V39)
|
||||||
|
reasoning_tokens INTEGER, -- 思考 token (V39)
|
||||||
|
UNIQUE(conversation_id, seq)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 灵感评估历史 (V22 建表 / V25 补唯一约束) — 追加型审计,version 单调递增
|
||||||
|
CREATE TABLE idea_evaluations (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
idea_id TEXT NOT NULL,
|
||||||
|
version INTEGER NOT NULL,
|
||||||
|
ai_analysis TEXT,
|
||||||
|
scores TEXT,
|
||||||
|
score REAL,
|
||||||
|
evaluated_by TEXT,
|
||||||
|
evaluated_at TEXT NOT NULL,
|
||||||
|
UNIQUE(idea_id, version) -- 评估版本并发重复兜底 (V25)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 任务横向关联 (V29 建表) — 任务间 depends_on/blocks/relates_to,循环检测走应用层
|
||||||
|
CREATE TABLE task_links (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
source_id TEXT NOT NULL REFERENCES tasks(id),
|
||||||
|
target_id TEXT NOT NULL REFERENCES tasks(id),
|
||||||
|
link_type TEXT NOT NULL,
|
||||||
|
remark TEXT,
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 项目事件流 (V30 建表) — 跨实体统一事件,追加型审计(只增不改)
|
||||||
|
CREATE TABLE project_events (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT NOT NULL REFERENCES projects(id),
|
||||||
|
event_type TEXT NOT NULL,
|
||||||
|
entity_type TEXT,
|
||||||
|
entity_id TEXT,
|
||||||
|
from_state TEXT,
|
||||||
|
to_state TEXT,
|
||||||
|
context_json TEXT,
|
||||||
|
source TEXT, -- ai/human/system
|
||||||
|
conversation_id TEXT,
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 项目基础设施配置 (V31 建表) — 记录"项目用了什么基础设施",不存敏感凭证
|
||||||
|
CREATE TABLE project_services (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT NOT NULL REFERENCES projects(id),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
service_type TEXT NOT NULL, -- mysql/postgresql/sqlite/redis/mongodb/mq/api/other
|
||||||
|
endpoint TEXT,
|
||||||
|
config_json TEXT,
|
||||||
|
environment TEXT NOT NULL DEFAULT 'development',
|
||||||
|
remark TEXT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 工程 (V34 建表 / V40 补 description + status) — 项目下多工程(Monorepo/微服务/前后端分离)
|
||||||
|
CREATE TABLE project_modules (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT NOT NULL REFERENCES projects(id),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
git_url TEXT,
|
||||||
|
stack TEXT,
|
||||||
|
auto_detected BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
description TEXT, -- 工程职责描述 (V40)
|
||||||
|
status TEXT, -- active/archived (V40)
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 工程依赖 (V35 建表) — 工程间依赖边,支撑依赖图
|
||||||
|
CREATE TABLE module_dependencies (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT NOT NULL REFERENCES projects(id),
|
||||||
|
from_module_id TEXT NOT NULL REFERENCES project_modules(id),
|
||||||
|
to_module_id TEXT NOT NULL REFERENCES project_modules(id),
|
||||||
|
dep_type TEXT NOT NULL DEFAULT 'library', -- library/api/mq/shared/custom
|
||||||
|
label TEXT,
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 多 Agent 并行执行 (V36 建表) — ai_plans/ai_subtasks/ai_conflicts 三表 + ai_messages/ai_tool_executions 补 subtask_id
|
||||||
|
CREATE TABLE ai_plans (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
conversation_id TEXT NOT NULL,
|
||||||
|
user_message_id TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'planning',
|
||||||
|
subtask_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
completed_at TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE ai_subtasks (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
plan_id TEXT NOT NULL REFERENCES ai_plans(id),
|
||||||
|
persona_id TEXT,
|
||||||
|
intent TEXT NOT NULL DEFAULT '',
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
layer INTEGER NOT NULL DEFAULT 0,
|
||||||
|
deps TEXT,
|
||||||
|
branch TEXT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
completed_at TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE ai_conflicts (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
plan_id TEXT NOT NULL REFERENCES ai_plans(id),
|
||||||
|
file_path TEXT NOT NULL DEFAULT '',
|
||||||
|
conflict_type TEXT NOT NULL DEFAULT 'file',
|
||||||
|
subtask_a TEXT,
|
||||||
|
subtask_b TEXT,
|
||||||
|
diff_a TEXT,
|
||||||
|
diff_b TEXT,
|
||||||
|
resolution TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
resolved_by TEXT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
resolved_at TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 对话版本化快照 (V37 建表)
|
||||||
|
CREATE TABLE conversation_checkpoints (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
conv_id TEXT NOT NULL,
|
||||||
|
snapshot TEXT NOT NULL,
|
||||||
|
token_total INTEGER NOT NULL,
|
||||||
|
label TEXT,
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
-- 内部表: schema_version (version INTEGER PRIMARY KEY) — 迁移版本记录
|
-- 内部表: schema_version (version INTEGER PRIMARY KEY) — 迁移版本记录
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Generated
+10
-25
@@ -12,6 +12,7 @@
|
|||||||
"@dcloudio/uni-components": "3.0.0-alpha-4080720251125001",
|
"@dcloudio/uni-components": "3.0.0-alpha-4080720251125001",
|
||||||
"@dcloudio/uni-h5": "3.0.0-alpha-4080720251125001",
|
"@dcloudio/uni-h5": "3.0.0-alpha-4080720251125001",
|
||||||
"@dcloudio/uni-mp-weixin": "3.0.0-alpha-4080720251125001",
|
"@dcloudio/uni-mp-weixin": "3.0.0-alpha-4080720251125001",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
"marked": "^18.0.5",
|
"marked": "^18.0.5",
|
||||||
"vue": "^3.4.0",
|
"vue": "^3.4.0",
|
||||||
"vue-i18n": "^9.0.0"
|
"vue-i18n": "^9.0.0"
|
||||||
@@ -4715,7 +4716,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4729,7 +4729,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4743,7 +4742,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4757,7 +4755,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4771,7 +4768,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4785,7 +4781,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4799,7 +4794,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4813,7 +4807,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4827,7 +4820,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4841,7 +4833,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4855,7 +4846,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4869,7 +4859,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4883,7 +4872,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4897,7 +4885,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4911,7 +4898,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4925,7 +4911,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4939,7 +4924,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4953,7 +4937,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4967,7 +4950,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4981,7 +4963,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4995,7 +4976,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5009,7 +4989,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5023,7 +5002,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5037,7 +5015,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -5051,7 +5028,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -8052,6 +8028,15 @@
|
|||||||
"he": "bin/he"
|
"he": "bin/he"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/highlight.js": {
|
||||||
|
"version": "11.11.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||||
|
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/html-encoding-sniffer": {
|
"node_modules/html-encoding-sniffer": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@dcloudio/uni-components": "3.0.0-alpha-4080720251125001",
|
"@dcloudio/uni-components": "3.0.0-alpha-4080720251125001",
|
||||||
"@dcloudio/uni-h5": "3.0.0-alpha-4080720251125001",
|
"@dcloudio/uni-h5": "3.0.0-alpha-4080720251125001",
|
||||||
"@dcloudio/uni-mp-weixin": "3.0.0-alpha-4080720251125001",
|
"@dcloudio/uni-mp-weixin": "3.0.0-alpha-4080720251125001",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
"marked": "^18.0.5",
|
"marked": "^18.0.5",
|
||||||
"vue": "^3.4.0",
|
"vue": "^3.4.0",
|
||||||
"vue-i18n": "^9.0.0"
|
"vue-i18n": "^9.0.0"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* marked HTML 后处理:为代码块/内联 code/链接/表格注入 inline style(P1-G)。
|
* marked HTML 后处理:为代码块/内联 code/链接/表格/图片/任务列表注入 inline style。
|
||||||
*
|
*
|
||||||
* 为什么后处理而非 marked 自定义 renderer:
|
* 为什么后处理而非 marked 自定义 renderer:
|
||||||
* - mp-weixin rich-text 不链接页面 CSS class,仅认节点 inline style → 必须把样式写进 HTML 属性。
|
* - mp-weixin rich-text 不链接页面 CSS class,仅认节点 inline style → 必须把样式写进 HTML 属性。
|
||||||
@@ -11,8 +11,88 @@
|
|||||||
* 跳转需第三方 mp-html(本次不做,记后续)。
|
* 跳转需第三方 mp-html(本次不做,记后续)。
|
||||||
* - rich-text 非滚动容器,overflow-y:auto/max-height 不创建滚动区 → 长代码靠
|
* - rich-text 非滚动容器,overflow-y:auto/max-height 不创建滚动区 → 长代码靠
|
||||||
* white-space:pre-wrap + word-break:break-all 换行防撑爆,不限高滚动。
|
* white-space:pre-wrap + word-break:break-all 换行防撑爆,不限高滚动。
|
||||||
|
* - <img> 网络图须域名进微信「downloadFile 合法域名」白名单,未配置时真机不显示
|
||||||
|
* (基础库限制,非渲染代码问题);开发者后台配白名单后即正常。
|
||||||
|
* - <input> 不在 rich-text 节点白名单 → GFM 任务列表勾选框被剥离成纯文本,见下替换。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// 语法高亮:highlight.js core + 按需注册常用语言(包体积敏感,不引全量)。
|
||||||
|
// 无 DOM 不可用 highlightElement,hljs 产出的 token class 由 inlineHljs 转 inline style。
|
||||||
|
import hljs from 'highlight.js/lib/core'
|
||||||
|
import javascript from 'highlight.js/lib/languages/javascript'
|
||||||
|
import typescript from 'highlight.js/lib/languages/typescript'
|
||||||
|
import json from 'highlight.js/lib/languages/json'
|
||||||
|
import xml from 'highlight.js/lib/languages/xml'
|
||||||
|
import css from 'highlight.js/lib/languages/css'
|
||||||
|
import markdown from 'highlight.js/lib/languages/markdown'
|
||||||
|
import rust from 'highlight.js/lib/languages/rust'
|
||||||
|
import go from 'highlight.js/lib/languages/go'
|
||||||
|
import python from 'highlight.js/lib/languages/python'
|
||||||
|
import java from 'highlight.js/lib/languages/java'
|
||||||
|
import bash from 'highlight.js/lib/languages/bash'
|
||||||
|
import sql from 'highlight.js/lib/languages/sql'
|
||||||
|
import yaml from 'highlight.js/lib/languages/yaml'
|
||||||
|
import diff from 'highlight.js/lib/languages/diff'
|
||||||
|
|
||||||
|
hljs.registerLanguage('javascript', javascript)
|
||||||
|
hljs.registerLanguage('typescript', typescript)
|
||||||
|
hljs.registerLanguage('json', json)
|
||||||
|
hljs.registerLanguage('xml', xml)
|
||||||
|
hljs.registerLanguage('css', css)
|
||||||
|
hljs.registerLanguage('markdown', markdown)
|
||||||
|
hljs.registerLanguage('rust', rust)
|
||||||
|
hljs.registerLanguage('go', go)
|
||||||
|
hljs.registerLanguage('python', python)
|
||||||
|
hljs.registerLanguage('java', java)
|
||||||
|
hljs.registerLanguage('bash', bash)
|
||||||
|
hljs.registerLanguage('sql', sql)
|
||||||
|
hljs.registerLanguage('yaml', yaml)
|
||||||
|
hljs.registerLanguage('diff', diff)
|
||||||
|
|
||||||
|
// 围栏 info 常见别名 → 已注册语言(hljs.getLanguage 自带部分别名,这里补齐业务常用)
|
||||||
|
const LANG_ALIASES: Record<string, string> = {
|
||||||
|
js: 'javascript', ts: 'typescript', py: 'python',
|
||||||
|
sh: 'bash', shell: 'bash', yml: 'yaml', md: 'markdown',
|
||||||
|
html: 'xml', vue: 'xml', svg: 'xml',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** hljs token class → inline style(rich-text 认 style 不认 class;色板对齐桌面 github-dark) */
|
||||||
|
const TOKEN_STYLE: Record<string, string> = {
|
||||||
|
'hljs-keyword': 'color:#ff7b72',
|
||||||
|
'hljs-doctag': 'color:#ff7b72',
|
||||||
|
'hljs-meta': 'color:#ff7b72',
|
||||||
|
'hljs-type': 'color:#ff7b72',
|
||||||
|
'hljs-template-tag': 'color:#ff7b72',
|
||||||
|
'hljs-template-variable': 'color:#ff7b72',
|
||||||
|
'hljs-title': 'color:#d2a8ff',
|
||||||
|
'hljs-attr': 'color:#79c0ff',
|
||||||
|
'hljs-attribute': 'color:#79c0ff',
|
||||||
|
'hljs-literal': 'color:#79c0ff',
|
||||||
|
'hljs-number': 'color:#79c0ff',
|
||||||
|
'hljs-operator': 'color:#79c0ff',
|
||||||
|
'hljs-variable': 'color:#79c0ff',
|
||||||
|
'hljs-selector-attr': 'color:#79c0ff',
|
||||||
|
'hljs-selector-class': 'color:#79c0ff',
|
||||||
|
'hljs-selector-id': 'color:#79c0ff',
|
||||||
|
'hljs-regexp': 'color:#a5d6ff',
|
||||||
|
'hljs-string': 'color:#a5d6ff',
|
||||||
|
'hljs-built_in': 'color:#ffa657',
|
||||||
|
'hljs-symbol': 'color:#ffa657',
|
||||||
|
'hljs-comment': 'color:#8b949e',
|
||||||
|
'hljs-code': 'color:#8b949e',
|
||||||
|
'hljs-formula': 'color:#8b949e',
|
||||||
|
'hljs-name': 'color:#7ee787',
|
||||||
|
'hljs-quote': 'color:#7ee787',
|
||||||
|
'hljs-selector-tag': 'color:#7ee787',
|
||||||
|
'hljs-selector-pseudo': 'color:#7ee787',
|
||||||
|
'hljs-section': 'color:#1f6feb;font-weight:bold',
|
||||||
|
'hljs-bullet': 'color:#f2cc60',
|
||||||
|
'hljs-addition': 'color:#aff5b4;background-color:#033a16',
|
||||||
|
'hljs-deletion': 'color:#ffdcd7;background-color:#67060c',
|
||||||
|
'hljs-emphasis': 'font-style:italic',
|
||||||
|
'hljs-strong': 'font-weight:bold',
|
||||||
|
}
|
||||||
|
|
||||||
/** 给指定标签注入 inline style(已有 style 则合并,无则新增)。 */
|
/** 给指定标签注入 inline style(已有 style 则合并,无则新增)。 */
|
||||||
function styleTag(html: string, tag: string, style: string): string {
|
function styleTag(html: string, tag: string, style: string): string {
|
||||||
const re = new RegExp(`<${tag}(\\s[^>]*)?>`, 'g')
|
const re = new RegExp(`<${tag}(\\s[^>]*)?>`, 'g')
|
||||||
@@ -28,22 +108,73 @@ function styleTag(html: string, tag: string, style: string): string {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 反转 marked 对代码块内容的 HTML 转义,还原原始代码文本供 hljs 高亮 */
|
||||||
|
function unescapeCode(s: string): string {
|
||||||
|
return s
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** hljs token class → inline style(无 DOM 不能 highlightElement,逐 span 转内联) */
|
||||||
|
function inlineHljs(html: string): string {
|
||||||
|
return html.replace(/<span class="([^"]+)">/g, (_m, cls: string) => {
|
||||||
|
const style = cls
|
||||||
|
.split(/\s+/)
|
||||||
|
.map((c) => TOKEN_STYLE[c])
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(';')
|
||||||
|
return style ? `<span style="${style}">` : '<span>'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 块代码语法高亮:仅处理已知语言,未知/无语言兜底原样(确定性强,不做自动猜测)。
|
||||||
|
* marked 已对代码内容做 HTML 转义,先反转还原再交给 hljs(hljs 输出自带转义)。
|
||||||
|
*/
|
||||||
|
function highlightCodeBlocks(html: string): string {
|
||||||
|
return html.replace(/<pre><code([^>]*)>([\s\S]*?)<\/code><\/pre>/gi, (m, attrs: string, inner: string) => {
|
||||||
|
const langMatch = /class="language-([^"]+)"/i.exec(attrs)
|
||||||
|
const langRaw = langMatch ? langMatch[1] : ''
|
||||||
|
const lang = LANG_ALIASES[langRaw] ?? langRaw
|
||||||
|
if (!lang || !hljs.getLanguage(lang)) return m
|
||||||
|
let value: string
|
||||||
|
try {
|
||||||
|
value = hljs.highlight(unescapeCode(inner), { language: lang, ignoreIllegals: true }).value
|
||||||
|
} catch {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
return `<pre><code${attrs}>${inlineHljs(value)}</code></pre>`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对 marked.parse 产出的 HTML 注入 inline style。
|
* 对 marked.parse 产出的 HTML 注入 inline style。
|
||||||
*
|
*
|
||||||
* 设计取舍:pre(块代码)给深色背景;<code> 不给背景(透明),仅等宽+橙色 —— 避免块代码内层 code
|
* 设计取舍:pre(块代码)给深色背景;<code> 不给背景(透明),仅等宽 + 橙色 —— 避免块代码内层 code
|
||||||
* 与 pre 背景冲突的双层色块(块代码内 code 透明继承 pre 深底,内联 code 橙色等宽在消息底上也可读)。
|
* 与 pre 背景冲突的双层色块(块代码内 code 透明继承 pre 深底,内联 code 橙色等宽在消息底上也可读);
|
||||||
|
* 块代码经 hljs 高亮后,关键字/字符串等 token 由内联色覆盖,未命中 token 的文本保留 code 基色。
|
||||||
*
|
*
|
||||||
* 正文颜色(P1-1 真机确认):rich-text 只认节点 inline style,不继承页面 CSS `.msg text`
|
* 正文颜色(真机确认):rich-text 只认节点 inline style,不继承页面 CSS `.msg text`
|
||||||
* (模板的 `.msg text{color:#e0e0e0}` 仅命中 `<text>` 子元素)。marked 产出的 p/h1-6/li/td/th
|
* (模板的 `.msg text{color:#e0e0e0}` 仅命中 `<text>` 子元素)。marked 产出的 p/h1-6/li/td/th
|
||||||
* 等不带 style 时,文本在暗色气泡上呈近黑 → 暗底黑字。故给承载正文的块级元素统一注入颜色。
|
* 等不带 style 时,文本在暗色气泡上呈近黑 → 暗底黑字。故给承载正文的块级元素统一注入颜色。
|
||||||
* strong/em/b 等行内元素继承父级(p/li)颜色,无需逐一注入。
|
* strong/em/b 等行内元素继承父级(p/li)颜色,无需逐一注入。
|
||||||
*/
|
*/
|
||||||
export function styleMarkdown(html: string): string {
|
export function styleMarkdown(html: string): string {
|
||||||
let out = html
|
let out = html
|
||||||
|
// 任务列表勾选框:rich-text 节点白名单不含 input,替换为字符符号(勾选/未勾选)
|
||||||
|
out = out.replace(/<input[^>]*type="checkbox"[^>]*>/gi, (m) => {
|
||||||
|
const checked = /checked/i.test(m)
|
||||||
|
const color = checked ? '#6bd99a' : '#666666'
|
||||||
|
return `<span style="color:${color};margin-right:4px">${checked ? '☑' : '☐'}</span>`
|
||||||
|
})
|
||||||
|
// 块代码语法高亮(须在下方 pre/code styleTag 前执行,hljs 输出 token span 后再合并基样式)
|
||||||
|
out = highlightCodeBlocks(out)
|
||||||
// 正文着色:块级文本容器统一浅色(rich-text 样式继承链从容器传子文本)
|
// 正文着色:块级文本容器统一浅色(rich-text 样式继承链从容器传子文本)
|
||||||
// word-break:break-all 防长 URL/长单词撑开气泡(rich-text 内文本不继承外层 .msg 的
|
// word-break:break-all 防长 URL/长单词撑开气泡(rich-text 内文本不继承外层 .msg 的
|
||||||
// word-break,须 inline 注入;对齐 mdRenderer 文件头"rich-text 只认 inline style")。
|
// word-break,须 inline 注入;对齐文件头"rich-text 只认 inline style")。
|
||||||
const BODY_STYLE = 'color:#e0e0e0;word-break:break-all'
|
const BODY_STYLE = 'color:#e0e0e0;word-break:break-all'
|
||||||
for (const tag of ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'blockquote', 'td']) {
|
for (const tag of ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'blockquote', 'td']) {
|
||||||
out = styleTag(out, tag, BODY_STYLE)
|
out = styleTag(out, tag, BODY_STYLE)
|
||||||
@@ -54,7 +185,7 @@ export function styleMarkdown(html: string): string {
|
|||||||
'pre',
|
'pre',
|
||||||
'display:block;background-color:#1a1a1a;color:#d4d4d4;padding:10px;border-radius:6px;white-space:pre-wrap;word-break:break-all;font-family:monospace;font-size:12px;margin:6px 0',
|
'display:block;background-color:#1a1a1a;color:#d4d4d4;padding:10px;border-radius:6px;white-space:pre-wrap;word-break:break-all;font-family:monospace;font-size:12px;margin:6px 0',
|
||||||
)
|
)
|
||||||
// code(块内 + 内联):等宽橙色,透明背景(避与 pre 双层色块)
|
// code(块内 + 内联):等宽橙色,透明背景(避与 pre 双层色块;块代码 token 由高亮色覆盖)
|
||||||
out = styleTag(
|
out = styleTag(
|
||||||
out,
|
out,
|
||||||
'code',
|
'code',
|
||||||
@@ -62,6 +193,9 @@ export function styleMarkdown(html: string): string {
|
|||||||
)
|
)
|
||||||
// 链接:蓝下划线(rich-text 不可点击,仅展示 + selectable 长按复制);word-break 防长 URL 撑开
|
// 链接:蓝下划线(rich-text 不可点击,仅展示 + selectable 长按复制);word-break 防长 URL 撑开
|
||||||
out = styleTag(out, 'a', 'color:#4a9eff;text-decoration:underline;word-break:break-all')
|
out = styleTag(out, 'a', 'color:#4a9eff;text-decoration:underline;word-break:break-all')
|
||||||
|
// 图片:max-width 防超宽撑爆气泡;block 去基线间隙;圆角与气泡一致。
|
||||||
|
// 网络图显示依赖微信「downloadFile 合法域名」白名单(见文件头),非渲染问题。
|
||||||
|
out = styleTag(out, 'img', 'max-width:100%;display:block;border-radius:6px')
|
||||||
// 表格:显式 table 布局 + 等宽字列。注意不能用 display:block(会失去表格布局语义、
|
// 表格:显式 table 布局 + 等宽字列。注意不能用 display:block(会失去表格布局语义、
|
||||||
// 列宽错乱),故显式声明 display:table;rich-text 非滚动容器(见文件头注释),无法建横向
|
// 列宽错乱),故显式声明 display:table;rich-text 非滚动容器(见文件头注释),无法建横向
|
||||||
// 滚动区 → 不做滚动容器,靠 max-width:100% + word-break:break-all 防超宽撑爆气泡;
|
// 滚动区 → 不做滚动容器,靠 max-width:100% + word-break:break-all 防超宽撑爆气泡;
|
||||||
|
|||||||
+41
-28
@@ -110,13 +110,13 @@ pub fn all_tools() -> &'static Vec<&'static ToolSpec> {
|
|||||||
spec("bind_directory", "为项目绑定本地代码目录(会做路径冲突检测,Medium 风险+审计日志)", object_schema(json!({"id": str_field("项目 ID"), "path": str_field("本地目录绝对路径")}), &["id", "path"]), Medium, bind_directory),
|
spec("bind_directory", "为项目绑定本地代码目录(会做路径冲突检测,Medium 风险+审计日志)", object_schema(json!({"id": str_field("项目 ID"), "path": str_field("本地目录绝对路径")}), &["id", "path"]), Medium, bind_directory),
|
||||||
// ─── 任务 ───
|
// ─── 任务 ───
|
||||||
spec("list_tasks", "列出所有未删除任务(可按 project_id/status 过滤;分页 offset/limit,默认 limit=50 上限 100)", object_schema(json!({"project_id": opt_str_field("按项目过滤(可空)"), "status": opt_str_field("按状态过滤(todo/in_progress/in_review/testing/blocked/done/cancelled,可空)"), "offset": int_field("偏移量(可空,默认 0)"), "limit": int_field("返回上限(可空,默认 50,上限 100)")}), &[]), Low, list_tasks),
|
spec("list_tasks", "列出所有未删除任务(可按 project_id/status 过滤;分页 offset/limit,默认 limit=50 上限 100)", object_schema(json!({"project_id": opt_str_field("按项目过滤(可空)"), "status": opt_str_field("按状态过滤(todo/in_progress/in_review/testing/blocked/done/cancelled,可空)"), "offset": int_field("偏移量(可空,默认 0)"), "limit": int_field("返回上限(可空,默认 50,上限 100)")}), &[]), Low, list_tasks),
|
||||||
spec("create_task", "创建任务(Medium 风险,默认允许+审计日志;可选 parent_id 父任务 ID,限 1 级嵌套,父任务自身不能是子任务)", object_schema(json!({"project_id": str_field("项目 ID"), "title": str_field("标题"), "description": str_field("描述"), "priority": int_field("优先级(可空,默认 0)"), "parent_id": opt_str_field("父任务 ID(可空)")}), &["project_id", "title"]), Medium, create_task),
|
spec("create_task", "创建任务(Medium 风险,默认允许+审计日志;可选 idea_id 关联灵感、queue 管理池(默认 todo,新建仅 backlog/todo/decision)、parent_id 父任务 ID(限 1 级嵌套,父任务自身不能是子任务)、content_json 结构化需求规格(须合法 JSON))", object_schema(json!({"project_id": str_field("项目 ID"), "title": str_field("标题"), "description": str_field("描述"), "priority": int_field("优先级(可空,默认 2=medium,值域 0..=3)"), "idea_id": opt_str_field("关联灵感 ID(可空)"), "queue": opt_str_field("管理池(可空,默认 todo,新建仅 backlog/todo/decision)"), "parent_id": opt_str_field("父任务 ID(可空,限 1 级嵌套)"), "content_json": opt_str_field("结构化需求规格 JSON(可空,须合法 JSON)")}), &["project_id", "title"]), Medium, create_task),
|
||||||
spec("update_task", "更新任务(部分更新:仅传需要改的字段,未传字段保留原值;状态须走 advance_task)", object_schema(json!({"id": str_field("任务 ID"), "project_id": opt_str_field("项目 ID(可空=保留原值)"), "title": opt_str_field("标题(可空=保留原值)"), "description": opt_str_field("描述(可空=保留原值)"), "expected_updated_at": int_field("乐观锁版本(可空):上次读取到的 updated_at 毫秒时间戳,不一致则拒绝写入")}), &["id"]), Medium, update_task),
|
spec("update_task", "更新任务(部分更新:仅传需要改的字段,未传字段保留原值;状态须走 advance_task)", object_schema(json!({"id": str_field("任务 ID"), "project_id": opt_str_field("项目 ID(可空=保留原值)"), "title": opt_str_field("标题(可空=保留原值)"), "description": opt_str_field("描述(可空=保留原值)"), "expected_updated_at": int_field("乐观锁版本(可空):上次读取到的 updated_at 毫秒时间戳,不一致则拒绝写入")}), &["id"]), Medium, update_task),
|
||||||
spec("advance_task", "推进任务状态(传目标 status,内部读当前态+状态机校验,Medium 风险+审计日志)", object_schema(json!({"id": str_field("任务 ID"), "to": str_field("目标 status(todo/in_progress/in_review/testing/blocked/done/cancelled)")}), &["id", "to"]), Medium, advance_task),
|
spec("advance_task", "推进任务状态(传目标 status,内部读当前态+状态机校验,Medium 风险+审计日志)", object_schema(json!({"id": str_field("任务 ID"), "to": str_field("目标 status(todo/in_progress/in_review/testing/blocked/done/cancelled)")}), &["id", "to"]), Medium, advance_task),
|
||||||
spec("delete_task", "软删任务(进回收站)——High 风险,默认拒绝,请在 DevFlow 应用内执行", object_schema(json!({"id": str_field("任务 ID")}), &["id"]), High, delete_task),
|
spec("delete_task", "软删任务(进回收站)——High 风险,默认拒绝,请在 DevFlow 应用内执行", object_schema(json!({"id": str_field("任务 ID")}), &["id"]), High, delete_task),
|
||||||
// ─── 灵感 ───
|
// ─── 灵感 ───
|
||||||
spec("list_ideas", "列出所有想法/灵感(分页:offset/limit,默认 limit=50 上限 100)", object_schema(json!({"offset": int_field("偏移量(可空,默认 0)"), "limit": int_field("返回上限(可空,默认 50,上限 100)")}), &[]), Low, list_ideas),
|
spec("list_ideas", "列出所有想法/灵感(分页:offset/limit,默认 limit=50 上限 100)", object_schema(json!({"offset": int_field("偏移量(可空,默认 0)"), "limit": int_field("返回上限(可空,默认 50,上限 100)")}), &[]), Low, list_ideas),
|
||||||
spec("create_idea", "创建想法(Medium 风险,默认允许+审计日志)", object_schema(json!({"title": str_field("标题"), "description": str_field("描述"), "priority": int_field("优先级(可空,默认 0)")}), &["title"]), Medium, create_idea),
|
spec("create_idea", "创建想法(Medium 风险,默认允许+审计日志)", object_schema(json!({"title": str_field("标题"), "description": str_field("描述"), "priority": int_field("优先级(可空,默认 1)")}), &["title"]), Medium, create_idea),
|
||||||
spec("update_idea", "更新想法(部分更新:仅传需要改的字段,未传字段保留原值)", object_schema(json!({"id": str_field("想法 ID"), "title": opt_str_field("标题(可空=保留原值)"), "description": opt_str_field("描述(可空=保留原值)"), "expected_updated_at": int_field("乐观锁版本(可空):上次读取到的 updated_at 毫秒时间戳,不一致则拒绝写入")}), &["id"]), Medium, update_idea),
|
spec("update_idea", "更新想法(部分更新:仅传需要改的字段,未传字段保留原值)", object_schema(json!({"id": str_field("想法 ID"), "title": opt_str_field("标题(可空=保留原值)"), "description": opt_str_field("描述(可空=保留原值)"), "expected_updated_at": int_field("乐观锁版本(可空):上次读取到的 updated_at 毫秒时间戳,不一致则拒绝写入")}), &["id"]), Medium, update_idea),
|
||||||
spec("delete_idea", "软删想法——High 风险,默认拒绝,请在 DevFlow 应用内执行", object_schema(json!({"id": str_field("想法 ID")}), &["id"]), High, delete_idea),
|
spec("delete_idea", "软删想法——High 风险,默认拒绝,请在 DevFlow 应用内执行", object_schema(json!({"id": str_field("想法 ID")}), &["id"]), High, delete_idea),
|
||||||
spec("evaluate_idea", "对想法做启发式评估(只读:只返分数不写库,基于 description/title 计算 feasibility/impact/urgency/overall)", object_schema(json!({"id": str_field("想法 ID")}), &["id"]), Low, evaluate_idea),
|
spec("evaluate_idea", "对想法做启发式评估(只读:只返分数不写库,基于 description/title 计算 feasibility/impact/urgency/overall)", object_schema(json!({"id": str_field("想法 ID")}), &["id"]), Low, evaluate_idea),
|
||||||
@@ -572,31 +572,45 @@ fn create_task(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> {
|
|||||||
Err(r) => return Box::pin(std::future::ready(r)),
|
Err(r) => return Box::pin(std::future::ready(r)),
|
||||||
};
|
};
|
||||||
let description = arg_str_or(&args, "description", "");
|
let description = arg_str_or(&args, "description", "");
|
||||||
let priority = arg_int_or(&args, "priority", 0);
|
|
||||||
medium_audit("create_task", &format!("{project_id}/{title}"));
|
medium_audit("create_task", &format!("{project_id}/{title}"));
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
// parent_id 可选(arg_str_or 给 "" 哨兵,空串视为 None)。非空时校验 1 级嵌套铁律:
|
|
||||||
// 父任务存在 + 父任务自身无 parent_id(防孙任务),违反返回明确错误(与 IPC create_task 同规则)。
|
|
||||||
let parent_id_raw = arg_str_or(&args, "parent_id", "");
|
|
||||||
let parent_id = if parent_id_raw.trim().is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let pid = parent_id_raw.trim();
|
|
||||||
let repo = TaskRepo::new(&db);
|
let repo = TaskRepo::new(&db);
|
||||||
match repo.get_by_id(pid).await {
|
|
||||||
Ok(Some(parent)) => {
|
// 共享校验(与 GUI/IPC 同源,一处修改全局生效):queue / parent_id / content_json / priority 值域
|
||||||
if parent.parent_id.is_some() {
|
let queue = match df_storage::crud::normalize_queue_for_create(
|
||||||
return CallToolResult::error(format!(
|
args.get("queue").and_then(|v| v.as_str()),
|
||||||
"父任务不能是子任务(1 级嵌套限制): {pid} 自身有 parent_id={:?}",
|
) {
|
||||||
parent.parent_id
|
Ok(q) => q,
|
||||||
));
|
Err(e) => return CallToolResult::error(e),
|
||||||
}
|
|
||||||
Some(pid.to_string())
|
|
||||||
}
|
|
||||||
Ok(None) => return CallToolResult::error(format!("父任务不存在: {pid}")),
|
|
||||||
Err(e) => return err_str(e),
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
let parent_id = match df_storage::crud::validate_parent_id(
|
||||||
|
&repo,
|
||||||
|
args.get("parent_id").and_then(|v| v.as_str()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(pid) => pid,
|
||||||
|
Err(e) => return CallToolResult::error(e),
|
||||||
|
};
|
||||||
|
let content_json = match df_storage::crud::validate_content_json(
|
||||||
|
args.get("content_json").and_then(|v| v.as_str()),
|
||||||
|
) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => return CallToolResult::error(e),
|
||||||
|
};
|
||||||
|
let priority = match df_storage::crud::normalize_priority(
|
||||||
|
args.get("priority").and_then(|v| v.as_i64()),
|
||||||
|
) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => return CallToolResult::error(e),
|
||||||
|
};
|
||||||
|
// 可选关联灵感(空串/缺省视为不关联,与 GUI create_task 同语义)
|
||||||
|
let idea_id = args
|
||||||
|
.get("idea_id")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(String::from);
|
||||||
|
|
||||||
let now = now_millis();
|
let now = now_millis();
|
||||||
let rec = TaskRecord {
|
let rec = TaskRecord {
|
||||||
id: new_id(),
|
id: new_id(),
|
||||||
@@ -611,15 +625,14 @@ fn create_task(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> {
|
|||||||
base_branch: None,
|
base_branch: None,
|
||||||
review_rounds: 0,
|
review_rounds: 0,
|
||||||
output_json: None,
|
output_json: None,
|
||||||
idea_id: None,
|
idea_id,
|
||||||
module_id: None,
|
module_id: None,
|
||||||
queue: "todo".to_string(),
|
queue,
|
||||||
parent_id,
|
parent_id,
|
||||||
content_json: None,
|
content_json,
|
||||||
created_at: now.clone(),
|
created_at: now.clone(),
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
};
|
};
|
||||||
let repo = TaskRepo::new(&db);
|
|
||||||
match repo.insert(rec).await {
|
match repo.insert(rec).await {
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
let created = repo.get_by_id(&id).await.ok().flatten();
|
let created = repo.get_by_id(&id).await.ok().flatten();
|
||||||
@@ -766,7 +779,7 @@ fn create_idea(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> {
|
|||||||
Err(r) => return Box::pin(std::future::ready(r)),
|
Err(r) => return Box::pin(std::future::ready(r)),
|
||||||
};
|
};
|
||||||
let description = arg_str_or(&args, "description", "");
|
let description = arg_str_or(&args, "description", "");
|
||||||
let priority = arg_int_or(&args, "priority", 0);
|
let priority = arg_int_or(&args, "priority", 1);
|
||||||
medium_audit("create_idea", &title);
|
medium_audit("create_idea", &title);
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let now = now_millis();
|
let now = now_millis();
|
||||||
|
|||||||
@@ -56,9 +56,7 @@ impl Node for ScriptNode {
|
|||||||
shell_type: Default::default(),
|
shell_type: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 命令执行安全:白/黑名单校验(从环境变量读取,逗号分隔命令名)。
|
// 命令执行安全:白/黑名单校验(运行时配置优先,回退环境变量/默认黑名单)。
|
||||||
// - 白名单非空时:命令首词不在白名单 → 直接拒绝执行
|
|
||||||
// - 黑名单匹配时:直接拒绝执行
|
|
||||||
// 命令名取首词(shell 第一段,如 `rm -rf /` 取 `rm`),按 trim + 小写规范化比较。
|
// 命令名取首词(shell 第一段,如 `rm -rf /` 取 `rm`),按 trim + 小写规范化比较。
|
||||||
let cmd_name = command.split_whitespace().next().unwrap_or("").to_lowercase();
|
let cmd_name = command.split_whitespace().next().unwrap_or("").to_lowercase();
|
||||||
if let Some(denied) = check_command_policy(&cmd_name) {
|
if let Some(denied) = check_command_policy(&cmd_name) {
|
||||||
@@ -70,7 +68,7 @@ impl Node for ScriptNode {
|
|||||||
anyhow::bail!("脚本命令被策略拒绝: {} (命令: {})", denied, command);
|
anyhow::bail!("脚本命令被策略拒绝: {} (命令: {})", denied, command);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 危险命令告警:匹配已知危险关键词,仅告警不阻止执行
|
// 危险关键词告警:匹配到仅告警不阻止执行(与硬拒绝的白/黑名单正交)
|
||||||
let dangerous_keywords = ["rm -rf", "DROP TABLE", "Format", "del /f", "shutdown"];
|
let dangerous_keywords = ["rm -rf", "DROP TABLE", "Format", "del /f", "shutdown"];
|
||||||
for &kw in &dangerous_keywords {
|
for &kw in &dangerous_keywords {
|
||||||
if command.contains(kw) {
|
if command.contains(kw) {
|
||||||
@@ -143,36 +141,30 @@ const DEFAULT_BLACKLIST: &[&str] = &["rm", "del", "format", "shutdown", "mkfs",
|
|||||||
|
|
||||||
/// 命令执行策略校验:从运行时配置 + 环境变量读取白/黑名单。
|
/// 命令执行策略校验:从运行时配置 + 环境变量读取白/黑名单。
|
||||||
///
|
///
|
||||||
/// 优先级(高→低):
|
/// 优先级(高→低):运行时配置(前端设置页写入) > 环境变量 > 默认黑名单兜底。
|
||||||
/// 1. 运行时配置(set_script_safety_config,前端设置页写入)
|
/// 黑名单优先于白名单(命中黑名单总是拒绝,即便同时在白名单);命令名比较前 trim + 小写。
|
||||||
/// 2. 环境变量 DF_SCRIPT_WHITELIST / DF_SCRIPT_BLACKLIST
|
|
||||||
/// 3. 默认黑名单兜底(DEFAULT_BLACKLIST)
|
|
||||||
///
|
|
||||||
/// 黑名单优先于白名单(黑名单匹配总是拒绝,即便同时在白名单)。
|
|
||||||
/// 命令名比较前 trim + ASCII 小写规范化;空段被忽略。
|
|
||||||
///
|
///
|
||||||
/// 返回 `Some(reason)` 表示拒绝;返回 `None` 表示放行。
|
/// 返回 `Some(reason)` 表示拒绝;返回 `None` 表示放行。
|
||||||
fn check_command_policy(cmd_name: &str) -> Option<&'static str> {
|
fn check_command_policy(cmd_name: &str) -> Option<&'static str> {
|
||||||
// 步骤1:尝试运行时黑名单(前端设置页写入,优先级最高)
|
// 运行时配置优先(前端设置页写入,优先级最高)
|
||||||
if let Some(raw) = RUNTIME_SCRIPT_BLACKLIST.get() {
|
if let Some(raw) = RUNTIME_SCRIPT_BLACKLIST.get() {
|
||||||
let list: Vec<&str> = raw.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
|
let list: Vec<&str> = raw.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
|
||||||
if list.iter().any(|c| c.eq_ignore_ascii_case(cmd_name)) {
|
if list.iter().any(|c| c.eq_ignore_ascii_case(cmd_name)) {
|
||||||
return Some("命令在黑名单中(运行时配置)");
|
return Some("命令在黑名单中(运行时配置)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 步骤2:尝试运行时白名单(非空时放行) — 仅当运行时黑名单未命中时才检查
|
// 运行时白名单非空时,命令须在名单内(命中即放行,跳过环境变量)
|
||||||
if let Some(raw) = RUNTIME_SCRIPT_WHITELIST.get() {
|
if let Some(raw) = RUNTIME_SCRIPT_WHITELIST.get() {
|
||||||
let list: Vec<&str> = raw.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
|
let list: Vec<&str> = raw.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
|
||||||
if !list.is_empty() && !list.iter().any(|c| c.eq_ignore_ascii_case(cmd_name)) {
|
if !list.is_empty() && !list.iter().any(|c| c.eq_ignore_ascii_case(cmd_name)) {
|
||||||
return Some("命令不在白名单中(运行时配置)");
|
return Some("命令不在白名单中(运行时配置)");
|
||||||
}
|
}
|
||||||
// 运行时白名单非空且命令在其中 → 直接放行(跳过 env var)
|
|
||||||
if !list.is_empty() {
|
if !list.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 步骤3:回退到环境变量(兼容旧部署)
|
// 回退环境变量(兼容旧部署);黑名单缺省用默认黑名单兜底
|
||||||
let env_blacklist = std::env::var("DF_SCRIPT_BLACKLIST").ok();
|
let env_blacklist = std::env::var("DF_SCRIPT_BLACKLIST").ok();
|
||||||
let blacklist: Vec<String> = match env_blacklist {
|
let blacklist: Vec<String> = match env_blacklist {
|
||||||
Some(raw) => raw
|
Some(raw) => raw
|
||||||
@@ -186,7 +178,7 @@ fn check_command_policy(cmd_name: &str) -> Option<&'static str> {
|
|||||||
return Some("命令在黑名单中");
|
return Some("命令在黑名单中");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 白名单:非空时命令名必须在白名单中才放行
|
// 环境变量白名单非空时,命令名须在名单内
|
||||||
if let Ok(whitelist_raw) = std::env::var("DF_SCRIPT_WHITELIST") {
|
if let Ok(whitelist_raw) = std::env::var("DF_SCRIPT_WHITELIST") {
|
||||||
let whitelist: Vec<String> = whitelist_raw
|
let whitelist: Vec<String> = whitelist_raw
|
||||||
.split(',')
|
.split(',')
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
//! SubflowNode — 嵌套子工作流节点
|
//! 子工作流节点 — 加载一个子 DAG 并在当前执行上下文中递归执行
|
||||||
//!
|
//!
|
||||||
//! 加载一个子模板(DagDef)并在当前执行上下文中递归执行。
|
|
||||||
//! 用于复用通用流程(如"代码审查"可作为任意主流程的子步骤)。
|
//! 用于复用通用流程(如"代码审查"可作为任意主流程的子步骤)。
|
||||||
|
//! 执行结果不直接展开节点,而是返回子 DAG 的 JSON 快照,由 DagExecutor 消费展开。
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use df_workflow::dag_def::DagDef;
|
use df_workflow::dag_def::DagDef;
|
||||||
use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema};
|
use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// SubflowNode 配置
|
/// 子工作流配置
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
struct SubflowConfig {
|
struct SubflowConfig {
|
||||||
/// 子工作流 DAG 定义(内联 JSON)
|
/// 子工作流 DAG 定义(内联 JSON)
|
||||||
@@ -21,13 +21,13 @@ struct SubflowConfig {
|
|||||||
/// 最大递归深度(安全阀)
|
/// 最大递归深度(安全阀)
|
||||||
const MAX_DEPTH: u32 = 10;
|
const MAX_DEPTH: u32 = 10;
|
||||||
|
|
||||||
/// SubflowNode — 嵌套子工作流
|
/// 子工作流节点
|
||||||
pub struct SubflowNode;
|
pub struct SubflowNode;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Node for SubflowNode {
|
impl Node for SubflowNode {
|
||||||
async fn execute(&self, ctx: NodeContext) -> NodeResult {
|
async fn execute(&self, ctx: NodeContext) -> NodeResult {
|
||||||
tracing::info!("SubflowNode 执行: {}", ctx.node_id);
|
tracing::info!("SubflowNode 执行: node_id={}", ctx.node_id);
|
||||||
|
|
||||||
let config: SubflowConfig = serde_json::from_value(ctx.config.clone())
|
let config: SubflowConfig = serde_json::from_value(ctx.config.clone())
|
||||||
.map_err(|e| anyhow::anyhow!("SubflowNode 配置解析失败: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("SubflowNode 配置解析失败: {}", e))?;
|
||||||
@@ -84,11 +84,20 @@ impl Node for SubflowNode {
|
|||||||
},
|
},
|
||||||
"required": ["dag"]
|
"required": ["dag"]
|
||||||
}),
|
}),
|
||||||
output: serde_json::json!({}),
|
output: serde_json::json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"subflow": { "type": "boolean" },
|
||||||
|
"node_count": { "type": "integer" },
|
||||||
|
"edge_count": { "type": "integer" },
|
||||||
|
"max_depth": { "type": "integer" },
|
||||||
|
"dag": { "type": "object" }
|
||||||
|
}
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn node_type(&self) -> &'static str {
|
fn node_type(&self) -> &str {
|
||||||
"subflow"
|
"subflow"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
//! - [`mod@settings`]:SettingsRepo + 列白名单(allowed_columns_for/validate_column_name/is_allowed_column)
|
//! - [`mod@settings`]:SettingsRepo + 列白名单(allowed_columns_for/validate_column_name/is_allowed_column)
|
||||||
//! - [`mod@project_repo`]:ProjectRepo/BranchRepo/ReleaseRepo/WorkflowRepo/NodeExecutionRepo
|
//! - [`mod@project_repo`]:ProjectRepo/BranchRepo/ReleaseRepo/WorkflowRepo/NodeExecutionRepo
|
||||||
//! - [`mod@task_repo`]:TaskRepo(含 advance_status_atomic 状态机收口)
|
//! - [`mod@task_repo`]:TaskRepo(含 advance_status_atomic 状态机收口)
|
||||||
|
//! - [`mod@task_validation`]:任务创建参数校验(queue 白名单/parent_id 嵌套铁律/content_json),GUI 与 MCP 工具共享
|
||||||
//! - [`mod@task_link_repo`]:TaskLinkRepo(任务横向关联 task_links,V29,知识图谱 Phase 1)
|
//! - [`mod@task_link_repo`]:TaskLinkRepo(任务横向关联 task_links,V29,知识图谱 Phase 1)
|
||||||
//! - [`mod@project_event_repo`]:ProjectEventRepo(统一事件流 project_events,V30,知识图谱 Phase 2)
|
//! - [`mod@project_event_repo`]:ProjectEventRepo(统一事件流 project_events,V30,知识图谱 Phase 2)
|
||||||
//! - [`mod@project_service_repo`]:ProjectServiceRepo(基础设施配置 project_services,V31,知识图谱 Phase 3)
|
//! - [`mod@project_service_repo`]:ProjectServiceRepo(基础设施配置 project_services,V31,知识图谱 Phase 3)
|
||||||
@@ -31,6 +32,7 @@ mod project_service_repo;
|
|||||||
mod settings;
|
mod settings;
|
||||||
mod task_link_repo;
|
mod task_link_repo;
|
||||||
mod task_repo;
|
mod task_repo;
|
||||||
|
mod task_validation;
|
||||||
|
|
||||||
pub use conversation_repo::*;
|
pub use conversation_repo::*;
|
||||||
pub use idea_eval_repo::*;
|
pub use idea_eval_repo::*;
|
||||||
@@ -43,6 +45,7 @@ pub use project_service_repo::*;
|
|||||||
pub use settings::*;
|
pub use settings::*;
|
||||||
pub use task_link_repo::*;
|
pub use task_link_repo::*;
|
||||||
pub use task_repo::*;
|
pub use task_repo::*;
|
||||||
|
pub use task_validation::*;
|
||||||
pub use module_dependency_repo::*;
|
pub use module_dependency_repo::*;
|
||||||
pub use plan_repo::*;
|
pub use plan_repo::*;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
//! 任务创建参数校验(共享层)
|
||||||
|
//!
|
||||||
|
//! GUI AI 工具(ai/tools/task.rs)与 MCP 工具(crates/df-mcp/src/tools.rs)曾各自内联
|
||||||
|
//! 一套 create_task 校验(queue 白名单 / parent_id 嵌套铁律 / content_json 合法性),
|
||||||
|
//! 演进易漂移。抽到本模块后两套工具同源复用,一处修改全局生效。
|
||||||
|
|
||||||
|
use crate::crud::task_repo::TaskRepo;
|
||||||
|
|
||||||
|
/// queue 合法值白名单(queue 是独立管理维度,与 status 执行维度正交)
|
||||||
|
pub const TASK_QUEUE_VALUES: &[&str] = &["backlog", "todo", "decision", "active", "done"];
|
||||||
|
|
||||||
|
/// 默认优先级(medium)— 新任务默认中优先级,与 IPC create_task 一致
|
||||||
|
pub const DEFAULT_TASK_PRIORITY: i32 = 2;
|
||||||
|
|
||||||
|
/// 校验并归一化 create_task 的 queue:空串/缺省 → "todo";
|
||||||
|
/// 白名单校验 + create 一致性(新建仅 backlog/todo/decision,active/done 须经 move_task_queue 流转)。
|
||||||
|
pub fn normalize_queue_for_create(queue: Option<&str>) -> Result<String, String> {
|
||||||
|
match queue {
|
||||||
|
None | Some("") => Ok("todo".to_string()),
|
||||||
|
Some(q) => {
|
||||||
|
let q = q.trim();
|
||||||
|
if !TASK_QUEUE_VALUES.contains(&q) {
|
||||||
|
return Err(format!("非法 queue 值 {:?},合法值: {:?}", q, TASK_QUEUE_VALUES));
|
||||||
|
}
|
||||||
|
match q {
|
||||||
|
"backlog" | "todo" | "decision" => Ok(q.to_string()),
|
||||||
|
other => Err(format!(
|
||||||
|
"新建任务 queue 不可直接落 {:?}(须经 move_task_queue 流转),当前仅允许 backlog/todo/decision",
|
||||||
|
other
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 校验 content_json(非空时须是合法 JSON),返回规范化值(None=未传)。
|
||||||
|
pub fn validate_content_json(content: Option<&str>) -> Result<Option<String>, String> {
|
||||||
|
match content {
|
||||||
|
Some(c) if !c.trim().is_empty() => {
|
||||||
|
serde_json::from_str::<serde_json::Value>(c)
|
||||||
|
.map_err(|e| format!("content_json 不是合法 JSON: {e}"))?;
|
||||||
|
Ok(Some(c.to_string()))
|
||||||
|
}
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 校验 parent_id 1 级嵌套铁律:父任务存在 + 父任务自身不是子任务(防孙任务)。
|
||||||
|
/// 返回归一化 parent_id(空串/缺省 → None)。
|
||||||
|
pub async fn validate_parent_id(
|
||||||
|
repo: &TaskRepo,
|
||||||
|
parent_id: Option<&str>,
|
||||||
|
) -> Result<Option<String>, String> {
|
||||||
|
let pid = parent_id
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty());
|
||||||
|
if let Some(pid) = &pid {
|
||||||
|
let parent = repo
|
||||||
|
.get_by_id(pid)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.ok_or_else(|| format!("父任务 {pid} 不存在,无法创建子任务"))?;
|
||||||
|
if parent.parent_id.is_some() {
|
||||||
|
return Err(format!(
|
||||||
|
"违反 1 级嵌套铁律:父任务 {pid} 自身是子任务(parent_id={:?}),不允许在其下创建孙任务",
|
||||||
|
parent.parent_id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 校验并归一化 create_task 的 priority:缺省 → DEFAULT_TASK_PRIORITY;越界值拒绝(值域 0..=3)。
|
||||||
|
pub fn normalize_priority(priority: Option<i64>) -> Result<i32, String> {
|
||||||
|
match priority {
|
||||||
|
None => Ok(DEFAULT_TASK_PRIORITY),
|
||||||
|
Some(p) if (0..=3).contains(&p) => Ok(p as i32),
|
||||||
|
Some(p) => Err(format!(
|
||||||
|
"非法 priority 值 {p},合法值: 整数 0..=3(0=critical, 1=high, 2=medium, 3=low)"
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,33 @@
|
|||||||
# df-nodes 节点集合
|
# df-nodes 节点集合
|
||||||
|
|
||||||
> 创建: 2026-06-10 | 最后更新: 2026-06-15
|
> 创建: 2026-06-10 | 最后更新: 2026-08-08
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
df-nodes 提供 DevFlow 工作流引擎的内置节点。所有节点实现 df-workflow 的 `Node` trait。
|
df-nodes 提供 DevFlow 工作流引擎的内置节点。所有节点实现 df-workflow 的 `Node` trait
|
||||||
|
(`execute` / `schema` / `node_type`),由 DAG Executor 按拓扑序驱动。
|
||||||
|
|
||||||
## 当前状态
|
## 当前状态
|
||||||
|
|
||||||
3 种节点全部完整实现(`execute()` 非 stub,有真实逻辑 + 单测覆盖)。
|
10 个节点全部完整实现(`execute()` 非 stub,有真实逻辑 + 单测覆盖),另有 `task_state_machine`
|
||||||
|
(状态机)与 `task_workflow_templates`(流程模板)两个支撑模块。
|
||||||
|
|
||||||
## 节点清单
|
## 节点清单
|
||||||
|
|
||||||
| 节点 | 功能 | 阻塞 | node_type | 实现状态 |
|
| 节点 | 功能 | 阻塞 | node_type | 实现状态 |
|
||||||
|------|------|------|-----------|---------|
|
|------|------|------|-----------|---------|
|
||||||
| AiNode | 调用 LLM 完成文本生成/分析(非流式 complete) | 否 | `ai` | ✅ 完整 |
|
| AiNode | 调用 LLM 完成文本生成/分析(非流式 complete) | 否 | `ai` | ✅ 完整 |
|
||||||
| ScriptNode | Shell/脚本执行 | 否 | `script` | ✅ 完整 |
|
| AiSelfReviewNode | AI 对任务产出自审(复用 AiNode 调用链,verdict 闸门) | 否 | `ai_self_review` | ✅ 完整 |
|
||||||
| HumanNode | 人工审批/确认(单选/多选) | 是 | `human` | ✅ 完整 |
|
| ScriptNode | Shell/脚本执行(白/黑名单安全策略) | 否 | `script` | ✅ 完整 |
|
||||||
|
| HumanNode | 人工审批/确认(单选/多选) | 是 | `human` | ✅ 完整 |
|
||||||
> 历史文档曾列 8 节点(AI/Script/Docker/Git/Human/Notify/HTTP/Subflow)全标骨架。实际仅 AI/Script/Human 3 节点存在,其余 5 节点(Docker/Git/Notify/HTTP/Subflow)从未实现,已从本文档删除。
|
| TaskAdvanceNode | 任务状态推进链唯一 status 写入路径(状态机 + CAS) | 否 | `task_advance` | ✅ 完整 |
|
||||||
|
| GitNode | Git 操作(branch/checkout/commit/merge/push/status/log) | 否 | `git` | ✅ 完整 |
|
||||||
|
| HTTPNode | HTTP 请求(GET/POST/PUT/DELETE) | 否 | `http` | ✅ 完整 |
|
||||||
|
| NotifyNode | 通知(桌面日志 / webhook,尽力而为不阻断) | 否 | `notify` | ✅ 完整 |
|
||||||
|
| SubflowNode | 嵌套子工作流(返回子 DAG 快照,深度限制防递归) | 否 | `subflow` | ✅ 完整 |
|
||||||
|
| DockerNode | Docker 容器内执行命令(`docker run --rm`,环境检测+授权) | 否 | `docker` | ✅ 完整 |
|
||||||
|
|
||||||
## 节点详述
|
## 节点详述
|
||||||
|
|
||||||
@@ -35,12 +42,18 @@ df-nodes 提供 DevFlow 工作流引擎的内置节点。所有节点实现 df-w
|
|||||||
- **输出**:`{ text, model, usage: {prompt_tokens, completion_tokens, total_tokens} }`。
|
- **输出**:`{ text, model, usage: {prompt_tokens, completion_tokens, total_tokens} }`。
|
||||||
- **与 AI Chat 区别**:AiNode 由 DAG Executor 自动驱动(嵌入自动化链路),非交互对话。
|
- **与 AI Chat 区别**:AiNode 由 DAG Executor 自动驱动(嵌入自动化链路),非交互对话。
|
||||||
|
|
||||||
|
### AiSelfReviewNode(ai_self_review_node.rs)
|
||||||
|
|
||||||
|
AI 自审闭环节点:对 AiNode 产出的任务内容做二次 AI 审查,返回 `verdict`(通过/打回)闸门。
|
||||||
|
复用 AiNode 的 provider 调用链(`ai_node_helpers`),差异在 prompt 模板与 JSON 解析。
|
||||||
|
|
||||||
### ScriptNode(script_node.rs)
|
### ScriptNode(script_node.rs)
|
||||||
|
|
||||||
执行 Shell 脚本或自定义命令,复用 `df_execute::shell::execute`(跨平台:Windows `cmd /C` / Unix `sh -c`)。
|
执行 Shell 脚本或自定义命令,复用 `df_execute::shell::execute`(跨平台:Windows `cmd /C` / Unix `sh -c`)。
|
||||||
|
|
||||||
- **必填**:`command`。
|
- **必填**:`command`。
|
||||||
- **可选**:`timeout_secs`、`working_dir`。
|
- **可选**:`timeout_secs`、`working_dir`。
|
||||||
|
- **安全策略**:命令名(首词)白/黑名单校验(运行时配置优先,回退环境变量 / 默认黑名单);危险关键词仅告警不阻止。
|
||||||
- **失败语义**:非零退出码视为执行失败(`bail!` 带 exit_code + stderr)。
|
- **失败语义**:非零退出码视为执行失败(`bail!` 带 exit_code + stderr)。
|
||||||
- **输出**:`{ stdout, stderr, exit_code, duration_ms }`。
|
- **输出**:`{ stdout, stderr, exit_code, duration_ms }`。
|
||||||
|
|
||||||
@@ -49,7 +62,7 @@ df-nodes 提供 DevFlow 工作流引擎的内置节点。所有节点实现 df-w
|
|||||||
阻塞节点,订阅事件总线 → 发 `HumanApprovalRequest` → `select!` 轮询 `HumanApprovalResponse` / 超时 / 取消。
|
阻塞节点,订阅事件总线 → 发 `HumanApprovalRequest` → `select!` 轮询 `HumanApprovalResponse` / 超时 / 取消。
|
||||||
|
|
||||||
- **执行顺序**:先 `subscribe()` 再 `send(Request)`(broadcast 不回放,反序会丢 Response 死等到超时);`send` 必须 `await`(否则 Future 不 poll、Request 不进 channel)。
|
- **执行顺序**:先 `subscribe()` 再 `send(Request)`(broadcast 不回放,反序会丢 Response 死等到超时);`send` 必须 `await`(否则 Future 不 poll、Request 不进 channel)。
|
||||||
- **审批模式**(F-260615-01):`select_type` 缺省 `Single`,非 `"multiple"` 一律按 Single 处理。
|
- **审批模式**:`select_type` 缺省 `Single`,非 `"multiple"` 一律按 Single 处理。
|
||||||
- Single → 决策数必须 = 1
|
- Single → 决策数必须 = 1
|
||||||
- Multiple → 决策数必须 ≥ 1
|
- Multiple → 决策数必须 ≥ 1
|
||||||
- `options` 空 → 允许自由文本(仅受数量约束);非空 → 每项必须 ∈ options
|
- `options` 空 → 允许自由文本(仅受数量约束);非空 → 每项必须 ∈ options
|
||||||
@@ -59,25 +72,66 @@ df-nodes 提供 DevFlow 工作流引擎的内置节点。所有节点实现 df-w
|
|||||||
- **取消**:每 500ms tick 检查 `node_status.is_cancelled`。
|
- **取消**:每 500ms tick 检查 `node_status.is_cancelled`。
|
||||||
- **输出**:`{ decision(首项,向后兼容), decisions(数组), comment }`。
|
- **输出**:`{ decision(首项,向后兼容), decisions(数组), comment }`。
|
||||||
|
|
||||||
|
### TaskAdvanceNode(task_advance_node.rs)
|
||||||
|
|
||||||
|
任务推进链的唯一 status 写入路径(与 IPC/AI 工具/MCP 同源):读当前 TaskRecord → 状态机
|
||||||
|
`can_transition` 校验 → 原子写(下沉 SQL 防 TOCTOU),退回转换自动累加 `review_rounds`。
|
||||||
|
|
||||||
|
### GitNode(git_node.rs)
|
||||||
|
|
||||||
|
通过 `df_execute::shell::execute` 调用本地 git CLI,`working_dir` 指定仓库路径。
|
||||||
|
支持 branch / checkout / commit / merge / push / status / log,输出按 action 提取结构化字段。
|
||||||
|
|
||||||
|
### HTTPNode(http_node.rs)
|
||||||
|
|
||||||
|
使用 reqwest 发起 GET/POST/PUT/DELETE 请求。config 提供 `method/url/headers/body/timeout_secs`,
|
||||||
|
输出 `status_code + body + content_type`。
|
||||||
|
|
||||||
|
### NotifyNode(notify_node.rs)
|
||||||
|
|
||||||
|
桌面(当前为日志占位,后续接 tauri-plugin-notification)/ webhook 通知。语义:尽力而为,
|
||||||
|
通知失败不阻断工作流(输出 `success=false` 但仍 Ok 返回)。
|
||||||
|
|
||||||
|
### SubflowNode(subflow_node.rs)
|
||||||
|
|
||||||
|
加载一个子 DAG 并在当前执行上下文中递归执行(复用通用流程,如「代码审查」作为子步骤)。
|
||||||
|
返回子 DAG 的 JSON 快照供 DagExecutor 消费展开,`max_depth` 限制嵌套深度防无限递归。
|
||||||
|
|
||||||
|
### DockerNode(docker_node.rs)
|
||||||
|
|
||||||
|
通过 `docker run --rm` 一次性容器执行命令,复用 `df_execute::shell::execute` 调用本地 docker CLI。
|
||||||
|
含环境检测(本机是否有 docker)与授权校验。
|
||||||
|
|
||||||
## 文件结构
|
## 文件结构
|
||||||
|
|
||||||
```
|
```
|
||||||
crates/df-nodes/src/
|
crates/df-nodes/src/
|
||||||
├── lib.rs — 模块入口(声明 ai_node / human_node / script_node 三个 pub mod)
|
├── lib.rs — 模块入口(声明各 pub mod)
|
||||||
├── ai_node.rs — AiNode(LLM 文本生成/分析)
|
├── ai_node.rs — AiNode(LLM 文本生成/分析)
|
||||||
|
├── ai_node_helpers.rs — AiNode 纯逻辑(provider 解析/参数构造,非 Node)
|
||||||
|
├── ai_self_review_node.rs — AiSelfReviewNode(AI 自审)
|
||||||
├── script_node.rs — ScriptNode(Shell 执行)
|
├── script_node.rs — ScriptNode(Shell 执行)
|
||||||
└── human_node.rs — HumanNode(人工审批/确认,阻塞)
|
├── human_node.rs — HumanNode(人工审批/确认,阻塞)
|
||||||
|
├── human_node_helpers.rs — HumanNode 纯逻辑(拒绝语义判定,非 Node)
|
||||||
|
├── task_advance_node.rs — TaskAdvanceNode(任务状态推进链)
|
||||||
|
├── task_state_machine.rs — 任务状态机(can_transition 等,非 Node)
|
||||||
|
├── task_workflow_templates.rs — 流程模板(testing 等模板,非 Node)
|
||||||
|
├── git_node.rs — GitNode(Git 操作)
|
||||||
|
├── http_node.rs — HttpNode(HTTP 请求)
|
||||||
|
├── notify_node.rs — NotifyNode(通知)
|
||||||
|
├── subflow_node.rs — SubflowNode(嵌套子工作流)
|
||||||
|
└── docker_node.rs — DockerNode(容器内执行)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 依赖关系
|
## 依赖关系
|
||||||
|
|
||||||
```
|
```
|
||||||
df-core
|
df-workflow (Node trait / DagDef)
|
||||||
← df-workflow (Node trait)
|
|
||||||
← df-nodes
|
← df-nodes
|
||||||
← df-ai (AiNode: build_provider / LlmProvider)
|
← df-ai (AiNode/AiSelfReviewNode: build_provider / LlmProvider)
|
||||||
← df-execute (ScriptNode: shell::execute)
|
← df-execute (ScriptNode/GitNode/DockerNode: shell::execute)
|
||||||
← df-core (HumanNode: events::WorkflowEvent / SelectType)
|
← df-storage (TaskAdvanceNode: TaskRepo::advance_status_atomic)
|
||||||
|
← df-core / df-types (HumanNode: events::WorkflowEvent / SelectType)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 相关文档
|
## 相关文档
|
||||||
|
|||||||
@@ -79,70 +79,39 @@ pub fn register(registry: &mut AiToolRegistry, db: &Arc<Database>) {
|
|||||||
let title = args["title"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 title"))?;
|
let title = args["title"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 title"))?;
|
||||||
let repo = df_storage::crud::TaskRepo::new(&db);
|
let repo = df_storage::crud::TaskRepo::new(&db);
|
||||||
|
|
||||||
// ── 知识图谱 Phase 1:queue/parent_id/content_json 校验(对齐 commands::task::create_task IPC)──
|
// 共享校验(与 MCP/IPC 同源,一处修改全局生效):queue 白名单+create 一致性 /
|
||||||
// queue:白名单 + 空串默认 todo。create_task 时 status 恒 todo,仅 backlog/todo/decision
|
// parent_id 1 级嵌套铁律 / content_json JSON 合法性 / priority 值域(0..=3,默认 2=medium)
|
||||||
// 合法(active/done 须经 move_task_queue 流转,新建直接落违反一致性约束)。
|
let queue = df_storage::crud::normalize_queue_for_create(
|
||||||
const TASK_QUEUE_VALUES: &[&str] = &["backlog", "todo", "decision", "active", "done"];
|
args.get("queue").and_then(|v| v.as_str()),
|
||||||
let queue = match args.get("queue").and_then(|v| v.as_str()) {
|
)
|
||||||
None | Some("") => "todo".to_string(),
|
.map_err(anyhow::Error::msg)?;
|
||||||
Some(q) => {
|
let parent_id = df_storage::crud::validate_parent_id(
|
||||||
let q = q.trim();
|
&repo,
|
||||||
if !TASK_QUEUE_VALUES.contains(&q) {
|
args.get("parent_id").and_then(|v| v.as_str()),
|
||||||
anyhow::bail!("非法 queue 值 {:?},合法值: {:?}", q, TASK_QUEUE_VALUES);
|
)
|
||||||
}
|
.await
|
||||||
// 一致性:create_task 时 status 恒 todo,仅 backlog/todo/decision 合法
|
.map_err(anyhow::Error::msg)?;
|
||||||
match q {
|
let content_json = df_storage::crud::validate_content_json(
|
||||||
"backlog" | "todo" | "decision" => q.to_string(),
|
args.get("content_json").and_then(|v| v.as_str()),
|
||||||
other => anyhow::bail!(
|
)
|
||||||
"新建任务 queue 不可直接落 {:?}(须经 move_task_queue 流转),当前仅允许 backlog/todo/decision",
|
.map_err(anyhow::Error::msg)?;
|
||||||
other
|
let priority = df_storage::crud::normalize_priority(
|
||||||
),
|
args.get("priority").and_then(|v| v.as_i64()),
|
||||||
}
|
)
|
||||||
}
|
.map_err(anyhow::Error::msg)?;
|
||||||
};
|
|
||||||
|
|
||||||
// parent_id:1 级嵌套铁律(对标设计 §2.1 D2)。空串/缺省=None;
|
|
||||||
// 非空时校验父任务存在 + 父任务自身无 parent_id(防孙任务)。
|
|
||||||
let parent_id = args
|
|
||||||
.get("parent_id")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.map(|s| s.trim().to_string())
|
|
||||||
.filter(|s| !s.is_empty());
|
|
||||||
if let Some(pid) = &parent_id {
|
|
||||||
let parent = repo
|
|
||||||
.get_by_id(pid)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("父任务 {pid} 不存在,无法创建子任务"))?;
|
|
||||||
if parent.parent_id.is_some() {
|
|
||||||
anyhow::bail!(
|
|
||||||
"违反 1 级嵌套铁律:父任务 {pid} 自身是子任务(parent_id={:?}),不允许在其下创建孙任务",
|
|
||||||
parent.parent_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// content_json:轻量 JSON 合法性校验(非空时须是合法 JSON),结构细节由 AI 消费层负责。
|
|
||||||
let content_json = match args.get("content_json").and_then(|v| v.as_str()) {
|
|
||||||
Some(c) if !c.trim().is_empty() => {
|
|
||||||
serde_json::from_str::<serde_json::Value>(c)
|
|
||||||
.map_err(|e| anyhow::anyhow!("content_json 不是合法 JSON: {e}"))?;
|
|
||||||
Some(c.to_string())
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let record = TaskRecord {
|
let record = TaskRecord {
|
||||||
id: new_id(), project_id: project_id.to_string(), title: title.to_string(),
|
id: new_id(), project_id: project_id.to_string(), title: title.to_string(),
|
||||||
description: args["description"].as_str().unwrap_or("").to_string(),
|
description: args["description"].as_str().unwrap_or("").to_string(),
|
||||||
// priority 默认 2(medium):与 commands::task::default_priority 一致,新任务默认中优先级(非 high)
|
// priority 默认 2(medium):与 commands::task::default_priority 一致,新任务默认中优先级(非 high)
|
||||||
status: TaskStatus::Todo, priority: args["priority"].as_i64().unwrap_or(2) as i32,
|
status: TaskStatus::Todo, priority,
|
||||||
branch_name: None, assignee: None, workflow_def_id: None, base_branch: None,
|
branch_name: None, assignee: None, workflow_def_id: None, base_branch: None,
|
||||||
review_rounds: 0,
|
review_rounds: 0,
|
||||||
output_json: None,
|
output_json: None,
|
||||||
// F-260619-01 可选关联灵感(空字符串/缺省视为不关联)
|
// F-260619-01 可选关联灵感(空字符串/缺省视为不关联)
|
||||||
idea_id: args.get("idea_id").and_then(|v| v.as_str()).filter(|s| !s.is_empty()).map(String::from),
|
idea_id: args.get("idea_id").and_then(|v| v.as_str()).filter(|s| !s.is_empty()).map(String::from),
|
||||||
module_id: args.get("module_id").and_then(|v| v.as_str()).filter(|s| !s.is_empty()).map(String::from),
|
module_id: args.get("module_id").and_then(|v| v.as_str()).filter(|s| !s.is_empty()).map(String::from),
|
||||||
// 知识图谱 Phase 1 V29 三列:经上方校验的 queue / parent_id / content_json
|
// 知识图谱 Phase 1 V29 三列:经上方共享校验的 queue / parent_id / content_json
|
||||||
queue,
|
queue,
|
||||||
parent_id,
|
parent_id,
|
||||||
content_json,
|
content_json,
|
||||||
|
|||||||
Reference in New Issue
Block a user