pi-web-ui 0.76.0 → 0.78.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -1
- package/dist/server/agent-service.js +120 -48
- package/dist/server/client-state.js +19 -3
- package/dist/server/delegate-task.js +174 -0
- package/dist/server/dsh/dsh-agent-service.js +57 -20
- package/dist/server/edit-soft-tool.js +3 -1
- package/dist/server/index.js +99 -1
- package/dist/server/markers/builtins/todo.js +2 -2
- package/dist/server/orchestrator.js +109 -0
- package/dist/server/prompt-composer.js +14 -3
- package/dist/server/queue-utils.js +18 -0
- package/dist/server/settings-service.js +67 -19
- package/dist/server/subagent-templates.js +251 -2
- package/dist/server/terminals.js +2 -10
- package/dist/server/tool-manager.js +217 -0
- package/package.json +1 -1
- package/web/dist/assets/TerminalPanel-DCQcbf-J.js +6 -0
- package/web/dist/assets/index-CaC1cuAF.css +10 -0
- package/web/dist/assets/index-SREmjZgY.js +346 -0
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +22 -7
- package/web/public/sw.js +22 -7
- package/web/dist/assets/TerminalPanel-CKvAH2c_.js +0 -6
- package/web/dist/assets/index-BBvR_HDI.js +0 -335
- package/web/dist/assets/index-ChcqaOWi.css +0 -10
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* delegate_task —— 结构化派单工具(oh-my-pi 委派协议的代码级硬化)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:单纯靠提示词要求模型“派单写详细”是没有强制力的——模型糊弄也不会有
|
|
5
|
+
* 任何后果。本工具把六段式派单格式写进参数 schema + 服务端校验:缺段/太短/
|
|
6
|
+
* 模板不可用直接报错打回(错误文本留在上下文里,模型补全后重试)。
|
|
7
|
+
*
|
|
8
|
+
* 与 subagent_spawn 的关系:并存。subagent_spawn 是通用自由派单;delegate_task
|
|
9
|
+
* 是走 specialist 模板的结构化派单(agent 必填且必须是启用的模板)。执行体复用
|
|
10
|
+
* 同一条 spawn 通道(SubagentToolHost.spawnSubagent):真子代理会话、白名单、
|
|
11
|
+
* 模型优先级、左栏徽标、等待/改向/停止全套机制都不用重写。
|
|
12
|
+
*
|
|
13
|
+
* 纯函数(validateDelegation / buildDelegationPrompt)可单测;语言按 ServerLang
|
|
14
|
+
* 切中英(section 头固定英文——子代理侧各模板早已习惯英文段头;面向派单者的
|
|
15
|
+
* 错误文本走 pick,key 缺失时自动回落英文,见 server/i18n.ts)。
|
|
16
|
+
*/
|
|
17
|
+
import { Type } from "typebox";
|
|
18
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { bilingual, pick } from "./i18n.js";
|
|
20
|
+
import { subagentTitle } from "./subagents.js";
|
|
21
|
+
/** 工具名(前端 ToolCallBlock 派单卡片靠它识别;改名需同步改前端)。 */
|
|
22
|
+
export const DELEGATE_TOOL_NAME = "delegate_task";
|
|
23
|
+
/** 各段最小长度(trim 后字符数):TASK 必须具体,OUTCOME 必须可验收,其余段不许空着。 */
|
|
24
|
+
const MIN_TASK = 20;
|
|
25
|
+
const MIN_OUTCOME = 10;
|
|
26
|
+
/** 六段的固定英文名(校验报错与拼装 prompt 共用,子代理侧无需翻译)。 */
|
|
27
|
+
const SECTIONS = ["TASK", "EXPECTED OUTCOME", "REQUIRED TOOLS", "MUST DO", "MUST NOT DO", "CONTEXT"];
|
|
28
|
+
function str(v) {
|
|
29
|
+
return typeof v === "string" ? v : "";
|
|
30
|
+
}
|
|
31
|
+
/** 把模型传进来的脏参数归一化(缺字段/错类型不抛错,交给校验报错)。 */
|
|
32
|
+
export function normalizeDelegation(params) {
|
|
33
|
+
const p = (params && typeof params === "object" ? params : {});
|
|
34
|
+
return {
|
|
35
|
+
agent: str(p.agent).trim(),
|
|
36
|
+
task: str(p.task),
|
|
37
|
+
expected_outcome: str(p.expected_outcome),
|
|
38
|
+
required_tools: str(p.required_tools),
|
|
39
|
+
must_do: str(p.must_do),
|
|
40
|
+
must_not_do: str(p.must_not_do),
|
|
41
|
+
context: str(p.context),
|
|
42
|
+
model: str(p.model).trim() || undefined,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 校验派单输入;通过返回 null,否则返回直接回给模型的错误文本(已按 lang 选好语言)。
|
|
47
|
+
* `usableNames` = 当前可用模板名(host.listTemplates(),只含 enabled 的)。
|
|
48
|
+
*/
|
|
49
|
+
export function validateDelegation(input, usableNames, lang = "en") {
|
|
50
|
+
if (!input.agent || !usableNames.includes(input.agent)) {
|
|
51
|
+
const shown = usableNames.slice(0, 12).join(" · ") + (usableNames.length > 12 ? " · …" : "");
|
|
52
|
+
return pick(lang, `派单被驳回:模板 "${input.agent || "(空)"}" 不可用(不存在或已停用)。可用模板:${shown || "(无)"}。用 subagent_templates 查简介再选;不要编造模板名。`, `Delegation rejected: template "${input.agent || "(empty)"}" is unavailable (missing or disabled). Available templates: ${shown || "(none)"}. Use subagent_templates for descriptions; do not invent template names.`, "delegate.validate.agent", { agent: input.agent, available: shown });
|
|
53
|
+
}
|
|
54
|
+
const checks = [
|
|
55
|
+
{ section: "TASK", text: input.task, min: MIN_TASK },
|
|
56
|
+
{ section: "EXPECTED OUTCOME", text: input.expected_outcome, min: MIN_OUTCOME },
|
|
57
|
+
{ section: "REQUIRED TOOLS", text: input.required_tools, min: 1 },
|
|
58
|
+
{ section: "MUST DO", text: input.must_do, min: 1 },
|
|
59
|
+
{ section: "MUST NOT DO", text: input.must_not_do, min: 1 },
|
|
60
|
+
{ section: "CONTEXT", text: input.context, min: 1 },
|
|
61
|
+
];
|
|
62
|
+
for (const c of checks) {
|
|
63
|
+
if (c.text.trim().length < c.min) {
|
|
64
|
+
return pick(lang, `派单被驳回:${c.section} 段太短(至少 ${c.min} 字,当前 ${c.text.trim().length} 字)。六段缺一不可、含糊不得:补全后重试,不要改用 subagent_spawn 绕过校验。`, `Delegation rejected: section ${c.section} is too short (minimum ${c.min} chars, got ${c.text.trim().length}). All six sections are mandatory and vague prompts fail: complete it and retry; do not bypass validation via subagent_spawn.`, "delegate.validate.short", { section: c.section, min: c.min });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/** 拼装发给子代理的标准六段 prompt(段头固定英文;收尾一行为汇报纪律)。 */
|
|
70
|
+
export function buildDelegationPrompt(input, lang = "en") {
|
|
71
|
+
const firstLine = input.task.split("\n")[0]?.trim().slice(0, 80) || input.agent;
|
|
72
|
+
const closer = lang === "zh"
|
|
73
|
+
? "汇报要简洁:做了什么、证据(路径/行号/输出)、遇到的问题、下一步建议。不扩大范围。"
|
|
74
|
+
: "Report back concisely: what was done, evidence (paths/line numbers/output), problems, suggested next steps. Do not expand scope.";
|
|
75
|
+
return [
|
|
76
|
+
`# ${input.agent}: ${firstLine}`,
|
|
77
|
+
``,
|
|
78
|
+
`## TASK`,
|
|
79
|
+
input.task.trim(),
|
|
80
|
+
``,
|
|
81
|
+
`## EXPECTED OUTCOME`,
|
|
82
|
+
input.expected_outcome.trim(),
|
|
83
|
+
``,
|
|
84
|
+
`## REQUIRED TOOLS`,
|
|
85
|
+
input.required_tools.trim(),
|
|
86
|
+
``,
|
|
87
|
+
`## MUST DO`,
|
|
88
|
+
input.must_do.trim(),
|
|
89
|
+
``,
|
|
90
|
+
`## MUST NOT DO`,
|
|
91
|
+
input.must_not_do.trim(),
|
|
92
|
+
``,
|
|
93
|
+
`## CONTEXT`,
|
|
94
|
+
input.context.trim(),
|
|
95
|
+
``,
|
|
96
|
+
`---`,
|
|
97
|
+
closer,
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
const delegateSchema = Type.Object({
|
|
101
|
+
agent: Type.String({
|
|
102
|
+
description: bilingual("Specialist template to delegate to (e.g. oracle, metis, momus, explore, librarian, sisyphus-junior, multimodal-looker, review). Must be an enabled template — use subagent_templates to see descriptions and pick the domain match.", "派单的目标 specialist 模板(如 oracle、metis、momus、explore、librarian、sisyphus-junior、multimodal-looker、review)。必须是启用的模板——先用 subagent_templates 看简介、按任务领域匹配。"),
|
|
103
|
+
}),
|
|
104
|
+
task: Type.String({
|
|
105
|
+
description: bilingual("Atomic, specific goal: ONE action per delegation (minimum 20 chars). Vague tasks are rejected.", "原子化具体目标:一次派单只做一件事(至少 20 字)。含糊的任务会被驳回。"),
|
|
106
|
+
}),
|
|
107
|
+
expected_outcome: Type.String({
|
|
108
|
+
description: bilingual("Concrete deliverables with done criteria: what does success look like (minimum 10 chars).", "具体交付物 + 完成标准:什么样算做完(至少 10 字)。"),
|
|
109
|
+
}),
|
|
110
|
+
required_tools: Type.String({
|
|
111
|
+
description: bilingual("Explicit tool whitelist for the subagent (prevents tool sprawl).", "子代理可用工具白名单(防工具乱用)。"),
|
|
112
|
+
}),
|
|
113
|
+
must_do: Type.String({
|
|
114
|
+
description: bilingual("Exhaustive requirements — leave NOTHING implicit.", "必须做的要求——写尽,不要留隐含项。"),
|
|
115
|
+
}),
|
|
116
|
+
must_not_do: Type.String({
|
|
117
|
+
description: bilingual("Forbidden actions — anticipate and block rogue behavior.", "禁止事项——预判并堵住乱发挥。"),
|
|
118
|
+
}),
|
|
119
|
+
context: Type.String({
|
|
120
|
+
description: bilingual("File paths, existing patterns, constraints the subagent must know.", "子代理必须知道的文件路径、既有模式、约束。"),
|
|
121
|
+
}),
|
|
122
|
+
model: Type.Optional(Type.String({
|
|
123
|
+
description: bilingual('Optional model "provider/id" for this delegation; omit = template model → panel default → follow the main conversation model.', "可选:本次派单的子代理模型(provider/id);不传 = 模板模型 → 面板默认 → 跟随主对话模型。"),
|
|
124
|
+
})),
|
|
125
|
+
});
|
|
126
|
+
/** 结构化派单工具:校验六段 → 拼装标准 prompt → 走 host.spawnSubagent 真子代理。 */
|
|
127
|
+
export function makeDelegateTaskTool(host, lang) {
|
|
128
|
+
const getLang = lang ?? host.lang ?? (() => "en");
|
|
129
|
+
const text = (t, details = {}) => ({
|
|
130
|
+
content: [{ type: "text", text: t }],
|
|
131
|
+
details,
|
|
132
|
+
});
|
|
133
|
+
return defineTool({
|
|
134
|
+
name: DELEGATE_TOOL_NAME,
|
|
135
|
+
label: "Delegate task",
|
|
136
|
+
description: bilingual("Delegate ONE well-defined task to a specialist subagent template with a structured six-section brief " +
|
|
137
|
+
"(TASK / EXPECTED OUTCOME / REQUIRED TOOLS / MUST DO / MUST NOT DO / CONTEXT). The brief is validated " +
|
|
138
|
+
"server-side: missing or vague sections are rejected with an error, so fill every section concretely. " +
|
|
139
|
+
"Prefer this over subagent_spawn when the work fits a specialist template. The delegation spawns a real " +
|
|
140
|
+
"subagent conversation (visible in the left running list); use subagent_wait_all / subagent_get_result to " +
|
|
141
|
+
"collect results, subagent_steer to redirect, subagent_stop to stop. For follow-ups continue the SAME " +
|
|
142
|
+
"subagent session instead of delegating again.", "把一个定义清楚的任务派给 specialist 子代理模板,派单文本是结构化六段 " +
|
|
143
|
+
"(TASK / EXPECTED OUTCOME / REQUIRED TOOLS / MUST DO / MUST NOT DO / CONTEXT)。六段在服务端校验:" +
|
|
144
|
+
"缺段或含糊直接报错打回,所以每段都要写实在。任务适合 specialist 模板时优先用它而不是 subagent_spawn。" +
|
|
145
|
+
"派单会启动真实子代理会话(左栏运行列表可见);用 subagent_wait_all / subagent_get_result 收结果、" +
|
|
146
|
+
"subagent_steer 改向、subagent_stop 停止。追问要在同一子代理会话里继续,不要重复派单。"),
|
|
147
|
+
promptSnippet: bilingual("Delegate a well-defined task to a specialist template with a validated six-section brief", "把定义清楚的任务派给 specialist 模板,六段派单文本带服务端校验"),
|
|
148
|
+
promptGuidelines: [
|
|
149
|
+
bilingual("Prefer delegate_task over subagent_spawn when the work matches a specialist template's domain", "任务落在 specialist 模板领域内时,优先用 delegate_task 而不是 subagent_spawn"),
|
|
150
|
+
bilingual("Before delegating, declare which template you chose and WHY its description matches the task", "派单前先声明选了哪个模板、它的简介与任务哪里匹配"),
|
|
151
|
+
bilingual("After delegation ALWAYS verify the result: does it work, does it follow codebase patterns, did it respect MUST DO / MUST NOT DO", "拿到派单结果必须验证:能跑吗、符合代码库模式吗、遵守 MUST DO / MUST NOT DO 了吗"),
|
|
152
|
+
bilingual("Never start implementing work that a pending delegated result was asked to decide", "已派出去待定的结论回来之前,不准先把相关的实现写了"),
|
|
153
|
+
],
|
|
154
|
+
parameters: delegateSchema,
|
|
155
|
+
execute: async (_id, params, _signal, _onUpdate, ctx) => {
|
|
156
|
+
const input = normalizeDelegation(params);
|
|
157
|
+
const usable = host.listTemplates().map((t) => t.name);
|
|
158
|
+
const err = validateDelegation(input, usable, getLang());
|
|
159
|
+
if (err)
|
|
160
|
+
return text(err, { delegated: false, agent: input.agent });
|
|
161
|
+
const prompt = buildDelegationPrompt(input, getLang());
|
|
162
|
+
const convId = await host.spawnSubagent(prompt, "delegate", ctx.cwd, input.agent, input.model);
|
|
163
|
+
const title = subagentTitle(prompt);
|
|
164
|
+
const modelLineZh = input.model ? `\n模型:${input.model}` : "";
|
|
165
|
+
const modelLineEn = input.model ? `\nModel: ${input.model}` : "";
|
|
166
|
+
return text(pick(getLang(), `已派单:${convId}\n模板:${input.agent} · 标题:${title}${modelLineZh}` +
|
|
167
|
+
`\n子代理已在左栏运行列表中。用 subagent_wait_all 一次等全部完成(不用轮询)、subagent_get_result 取单个结果;` +
|
|
168
|
+
`追问请在同一子代理会话里继续(subagent_steer),不要重复派单。拿到结果后必须验证再汇报。`, `Delegated: ${convId}\nTemplate: ${input.agent} · Title: ${title}${modelLineEn}` +
|
|
169
|
+
`\nThe subagent is in the left running list. Use subagent_wait_all to wait for all at once (no polling), ` +
|
|
170
|
+
`subagent_get_result for a single result; continue follow-ups in the SAME subagent session (subagent_steer), ` +
|
|
171
|
+
`do not delegate again. Verify the result before reporting.`, "delegate.started", { convId: convId, agent: input.agent, title: title, "input.model": input.model }), { convId, agent: input.agent, template: input.agent, model: input.model });
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -134,6 +134,9 @@ export class DshClientSession {
|
|
|
134
134
|
convs = new Map();
|
|
135
135
|
activeId = "";
|
|
136
136
|
convSeq = 0;
|
|
137
|
+
/** 待答问卷快照(见 attachRuntimeEvents 的 question.pending 与
|
|
138
|
+
* UiState.pendingQuestion):重连/刷新后由快照恢复对话框。 */
|
|
139
|
+
pendingQuestion = null;
|
|
137
140
|
/** 客户端级目标/审查偏好(跨会话共享的默认值,per-conversation goal 用它初始化)。 */
|
|
138
141
|
goalPrefs = { reviewModel: null, maxRounds: 2, locked: false };
|
|
139
142
|
sinks = new Set();
|
|
@@ -454,30 +457,39 @@ export class DshClientSession {
|
|
|
454
457
|
void this.answerQuestion(params0.id, [], true);
|
|
455
458
|
return;
|
|
456
459
|
}
|
|
460
|
+
const mapped = (params0.questions ?? []).map((q) => ({
|
|
461
|
+
id: String(q.id ?? ""),
|
|
462
|
+
question: String(q.question ?? ""),
|
|
463
|
+
...(typeof q.detail === "string"
|
|
464
|
+
? { detail: q.detail }
|
|
465
|
+
: {}),
|
|
466
|
+
...(typeof q.header === "string"
|
|
467
|
+
? { header: q.header }
|
|
468
|
+
: {}),
|
|
469
|
+
...(Array.isArray(q.options)
|
|
470
|
+
? {
|
|
471
|
+
options: q.options.map((o) => ({
|
|
472
|
+
label: String(o.label ?? ""),
|
|
473
|
+
...(typeof o.description === "string" ? { description: o.description } : {}),
|
|
474
|
+
...(typeof o.preview === "string" ? { preview: o.preview } : {}),
|
|
475
|
+
})),
|
|
476
|
+
}
|
|
477
|
+
: {}),
|
|
478
|
+
...(q.multiSelect ? { multiSelect: true } : {}),
|
|
479
|
+
}));
|
|
480
|
+
// 记下待答问卷:`question_pending` 只推给「当时在线」的连接,刷新页面
|
|
481
|
+
// /WS 重连后靠快照(UiState.pendingQuestion)把对话框恢复出来。
|
|
482
|
+
// DSH 的提问桥是 runtime 级的(无 conversationId),故不分对话。
|
|
483
|
+
this.pendingQuestion = {
|
|
484
|
+
id: params0.id,
|
|
485
|
+
...(typeof params0.deadline === "number" ? { deadline: params0.deadline } : {}),
|
|
486
|
+
questions: mapped,
|
|
487
|
+
};
|
|
457
488
|
this.emit({
|
|
458
489
|
type: "question_pending",
|
|
459
490
|
id: params0.id,
|
|
460
491
|
...(typeof params0.deadline === "number" ? { deadline: params0.deadline } : {}),
|
|
461
|
-
questions:
|
|
462
|
-
id: String(q.id ?? ""),
|
|
463
|
-
question: String(q.question ?? ""),
|
|
464
|
-
...(typeof q.detail === "string"
|
|
465
|
-
? { detail: q.detail }
|
|
466
|
-
: {}),
|
|
467
|
-
...(typeof q.header === "string"
|
|
468
|
-
? { header: q.header }
|
|
469
|
-
: {}),
|
|
470
|
-
...(Array.isArray(q.options)
|
|
471
|
-
? {
|
|
472
|
-
options: q.options.map((o) => ({
|
|
473
|
-
label: String(o.label ?? ""),
|
|
474
|
-
...(typeof o.description === "string" ? { description: o.description } : {}),
|
|
475
|
-
...(typeof o.preview === "string" ? { preview: o.preview } : {}),
|
|
476
|
-
})),
|
|
477
|
-
}
|
|
478
|
-
: {}),
|
|
479
|
-
...(q.multiSelect ? { multiSelect: true } : {}),
|
|
480
|
-
})),
|
|
492
|
+
questions: mapped,
|
|
481
493
|
});
|
|
482
494
|
}
|
|
483
495
|
else if (method === "tools.call.request") {
|
|
@@ -490,8 +502,24 @@ export class DshClientSession {
|
|
|
490
502
|
}
|
|
491
503
|
});
|
|
492
504
|
}
|
|
505
|
+
/** 快照侧的待答问卷(UiState.pendingQuestion):重连/刷新后靠它恢复对话框。
|
|
506
|
+
* DSH 的提问自带超时(goal-rpc 到点 reject),deadline 已过的不再下发——
|
|
507
|
+
* 否则已经没人等的问卷会被重连的客户端当成活的弹出来。标准引擎不限时,
|
|
508
|
+
* 没有 deadline,生命周期由回答/取消/dispose 精确终止。 */
|
|
509
|
+
pendingQuestionForSnapshot() {
|
|
510
|
+
const p = this.pendingQuestion;
|
|
511
|
+
if (!p)
|
|
512
|
+
return null;
|
|
513
|
+
if (p.deadline !== undefined && p.deadline <= Date.now())
|
|
514
|
+
return null;
|
|
515
|
+
return p;
|
|
516
|
+
}
|
|
493
517
|
/** 前端回答模型提问(question/answer → runtime 恢复工具结果)。 */
|
|
494
518
|
async answerQuestion(id, answers, cancelled) {
|
|
519
|
+
// 无论成功失败都清掉待答快照:同 id 不会再有下一次,留着会让重连的客户端
|
|
520
|
+
// 恢复到一张已经没人在等的问卷。
|
|
521
|
+
if (this.pendingQuestion?.id === id)
|
|
522
|
+
this.pendingQuestion = null;
|
|
495
523
|
try {
|
|
496
524
|
await this.runtime.answerQuestion(id, answers, cancelled);
|
|
497
525
|
}
|
|
@@ -1028,6 +1056,7 @@ export class DshClientSession {
|
|
|
1028
1056
|
thinkingLevel: this.thinkingLevel,
|
|
1029
1057
|
availableThinkingLevels: ["high"],
|
|
1030
1058
|
queue: { steering: conv.queue.steering, followUp: conv.queue.followUp },
|
|
1059
|
+
pendingQuestion: this.pendingQuestionForSnapshot(),
|
|
1031
1060
|
tools: [],
|
|
1032
1061
|
version: ++this.version,
|
|
1033
1062
|
piConfigured: !!loadDeepSeekKey(),
|
|
@@ -2191,6 +2220,8 @@ export class DshClientSession {
|
|
|
2191
2220
|
customSystemPrompt: this.settings.customSystemPrompt,
|
|
2192
2221
|
disabledSkills: this.settings.disabledSkills,
|
|
2193
2222
|
disabledExtensions: this.settings.disabledExtensions,
|
|
2223
|
+
// DSH engine: no unified tool gating (no subagent/edit_soft); empty keeps protocol complete.
|
|
2224
|
+
disabledAgentTools: [],
|
|
2194
2225
|
terminalToolsEnabled: this.settings.terminalToolsEnabled,
|
|
2195
2226
|
terminalBash: this.settings.terminalBash,
|
|
2196
2227
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
@@ -2201,6 +2232,8 @@ export class DshClientSession {
|
|
|
2201
2232
|
goalModeEnabled: this.settings.goalModeEnabled,
|
|
2202
2233
|
thinkingWrap: this.settings.thinkingWrap,
|
|
2203
2234
|
toolsWrap: this.settings.toolsWrap,
|
|
2235
|
+
// DSH 无 skill 全文注入概念,给空保协议完整。
|
|
2236
|
+
skillsFullText: [],
|
|
2204
2237
|
visionBridgeEnabled: false,
|
|
2205
2238
|
visionBridgeModel: null,
|
|
2206
2239
|
visionBridgePromptMode: "append",
|
|
@@ -2342,11 +2375,15 @@ export class DshClientSession {
|
|
|
2342
2375
|
promptOverrides: {},
|
|
2343
2376
|
disabledSkills: this.settings.disabledSkills,
|
|
2344
2377
|
disabledExtensions: this.settings.disabledExtensions,
|
|
2378
|
+
// DSH engine: no unified tool gating (no subagent/edit_soft); empty keeps protocol complete.
|
|
2379
|
+
disabledAgentTools: [],
|
|
2345
2380
|
terminalToolsEnabled: this.settings.terminalToolsEnabled,
|
|
2346
2381
|
terminalBash: this.settings.terminalBash,
|
|
2347
2382
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
2348
2383
|
editSoftEnabled: this.settings.editSoftEnabled,
|
|
2349
2384
|
// DSH 无独立重试配置,预设沿用默认值。
|
|
2385
|
+
// DSH 无 skill 全文注入概念,给空保预设类型完整。
|
|
2386
|
+
skillsFullText: [],
|
|
2350
2387
|
retryMaxAttempts: DEFAULT_RETRY_MAX_ATTEMPTS,
|
|
2351
2388
|
visionBridgePromptMode: "append",
|
|
2352
2389
|
visionBridgePrompt: "",
|
|
@@ -23,7 +23,9 @@ import { isAbsolute, join, resolve as nodeResolve } from "node:path";
|
|
|
23
23
|
import { Type } from "typebox";
|
|
24
24
|
import { defineTool, generateDiffString, generateUnifiedPatch, withFileMutationQueue, } from "@earendil-works/pi-coding-agent";
|
|
25
25
|
import { bilingual, pick } from "./i18n.js";
|
|
26
|
-
|
|
26
|
+
import { EDIT_SOFT_TOOL_NAME } from "./tool-manager.js";
|
|
27
|
+
/** 独立宽松编辑工具名(唯一登记见 tool-manager.ts;此处别名保兼容)。 */
|
|
28
|
+
export const SOFT_EDIT_TOOL_NAME = EDIT_SOFT_TOOL_NAME;
|
|
27
29
|
const replaceEditSchema = Type.Object({
|
|
28
30
|
oldText: Type.String({
|
|
29
31
|
description: bilingual("Text to replace. Loose matching: the content of each non-empty line (trimmed of leading/trailing whitespace) must match the corresponding file lines; leading-indentation (spaces/tabs) differences are ignored. Prefer whole lines/blocks.", "要替换的文本。宽松匹配:每个非空行的内容(去掉首尾空白)需与文件中对应行一致;行首缩进(空格/制表符)的差异会被忽略。建议按整行/整块提供。"),
|
package/dist/server/index.js
CHANGED
|
@@ -236,7 +236,12 @@ app.get("/api/file", async (req, res) => {
|
|
|
236
236
|
const name = basename(abs);
|
|
237
237
|
const kind = previewKind(name);
|
|
238
238
|
const isDownload = req.query.download === "1";
|
|
239
|
-
|
|
239
|
+
// HTML files preview through a sandboxed <iframe> in the file modal
|
|
240
|
+
// (FilePreview.tsx). They are text as far as previewKind goes, so
|
|
241
|
+
// allowlist them explicitly here.
|
|
242
|
+
const lower = name.toLowerCase();
|
|
243
|
+
const isHtmlPreview = lower.endsWith(".html") || lower.endsWith(".htm") || lower.endsWith(".xhtml");
|
|
244
|
+
if (!isDownload && kind !== "image" && kind !== "video" && !isHtmlPreview) {
|
|
240
245
|
res.status(400).end("not a previewable media file");
|
|
241
246
|
return;
|
|
242
247
|
}
|
|
@@ -251,6 +256,20 @@ app.get("/api/file", async (req, res) => {
|
|
|
251
256
|
res.download(abs, name);
|
|
252
257
|
}
|
|
253
258
|
else {
|
|
259
|
+
if (isHtmlPreview) {
|
|
260
|
+
// Sandbox even a top-level navigation to this URL: a workspace
|
|
261
|
+
// HTML file must never get our origin (it could otherwise read
|
|
262
|
+
// the token cookie). The modal iframe carries its own sandbox
|
|
263
|
+
// attribute as well (defense in depth).
|
|
264
|
+
//
|
|
265
|
+
// ?allowJs=1 is the explicit per-file opt-in from the preview
|
|
266
|
+
// modal ("启用脚本"): scripts run, but still in an opaque
|
|
267
|
+
// origin — no DOM/cookie/storage access to our app, no forms,
|
|
268
|
+
// no top-navigation. NEVER add allow-same-origin here.
|
|
269
|
+
const allowJs = req.query.allowJs === "1";
|
|
270
|
+
res.setHeader("Content-Security-Policy", allowJs ? "sandbox allow-scripts" : "sandbox");
|
|
271
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
272
|
+
}
|
|
254
273
|
res.sendFile(abs);
|
|
255
274
|
}
|
|
256
275
|
}
|
|
@@ -258,6 +277,68 @@ app.get("/api/file", async (req, res) => {
|
|
|
258
277
|
res.status(404).end("not found");
|
|
259
278
|
}
|
|
260
279
|
});
|
|
280
|
+
/**
|
|
281
|
+
* Directory-mapped preview: serves a workspace file at a URL that mirrors its
|
|
282
|
+
* directory location, so an HTML preview's RELATIVE subresources
|
|
283
|
+
* (<link href="../web/src/styles.css">, <img src="./x.png">, <script
|
|
284
|
+
* src="./app.js">, …) resolve and load with normal browser semantics. The
|
|
285
|
+
* iframe document URL itself carries the file's directory — no HTML rewriting.
|
|
286
|
+
*
|
|
287
|
+
* /api/preview/<workspace-rel-path>?clientId=…[&allowJs=1]
|
|
288
|
+
* /api/preview/__abs__/<absolute-wire-path>?clientId=…[&allowJs=1]
|
|
289
|
+
* (each path segment URI-encoded; ".." is normalized by the browser before
|
|
290
|
+
* the request is sent, workspace containment is still re-checked here)
|
|
291
|
+
*
|
|
292
|
+
* Same footing as /api/file: workspace containment enforced, HTML documents
|
|
293
|
+
* get a sandboxed CSP (?allowJs=1 relaxes scripts only — never same-origin),
|
|
294
|
+
* everything else streams with its real content type.
|
|
295
|
+
*/
|
|
296
|
+
app.get("/api/preview/*", async (req, res) => {
|
|
297
|
+
try {
|
|
298
|
+
const captured = String(req.params[0] ?? "");
|
|
299
|
+
const ABS_MARKER = "__abs__/";
|
|
300
|
+
const cid = typeof req.query.clientId === "string" ? req.query.clientId : "";
|
|
301
|
+
const cs = cid ? service.get(cid) : undefined;
|
|
302
|
+
const root = cs?.cwd ?? CWD;
|
|
303
|
+
// Express decodes %XX in the wildcard, so this is back to the wire
|
|
304
|
+
// form (filenames never contain "/", so per-segment encoding from
|
|
305
|
+
// the client round-trips exactly).
|
|
306
|
+
let abs;
|
|
307
|
+
if (captured === "__abs__" || captured.startsWith(ABS_MARKER)) {
|
|
308
|
+
// Machine browsing: absolute wire path ("C:/..." / "/...").
|
|
309
|
+
const wire = captured.slice(ABS_MARKER.length);
|
|
310
|
+
if (!isAbsoluteWirePath(wire)) {
|
|
311
|
+
res.status(400).end("bad absolute preview path");
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
abs = wireToAbs(wire);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
const wp = workspacePath(root, captured);
|
|
318
|
+
if (!wp) {
|
|
319
|
+
res.status(400).end("path outside workspace");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
abs = wp.abs;
|
|
323
|
+
}
|
|
324
|
+
const name = basename(abs);
|
|
325
|
+
const st = await stat(abs);
|
|
326
|
+
if (!st.isFile()) {
|
|
327
|
+
res.status(400).end("not a file");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
331
|
+
const lower = name.toLowerCase();
|
|
332
|
+
if (lower.endsWith(".html") || lower.endsWith(".htm") || lower.endsWith(".xhtml")) {
|
|
333
|
+
const allowJs = req.query.allowJs === "1";
|
|
334
|
+
res.setHeader("Content-Security-Policy", allowJs ? "sandbox allow-scripts" : "sandbox");
|
|
335
|
+
}
|
|
336
|
+
res.sendFile(abs);
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
res.status(404).end("not found");
|
|
340
|
+
}
|
|
341
|
+
});
|
|
261
342
|
// Production: serve the built frontend from web/dist. Resolve relative to this
|
|
262
343
|
// module so it works when installed as a package (global/npx/Docker), not just
|
|
263
344
|
// from the repo root. In dev, Vite serves the UI on :5173 and proxies /ws.
|
|
@@ -402,6 +483,21 @@ if (existsSync(webDist)) {
|
|
|
402
483
|
}
|
|
403
484
|
},
|
|
404
485
|
}));
|
|
486
|
+
// 缺失的静态文件必须 404(不能落进下面的 SPA catch-all):缺少的 hash 产物若
|
|
487
|
+
// 回 index.html(200),浏览器会把 HTML 当 JS/CSS 执行失败黑屏,SW 还会把
|
|
488
|
+
// 它按 200 缓进 STATIC_CACHE,之后即使文件恢复也要清缓存才能好。
|
|
489
|
+
app.use((req, res, next) => {
|
|
490
|
+
const p = req.path;
|
|
491
|
+
if (p.startsWith("/assets/") ||
|
|
492
|
+
p.startsWith("/icons/") ||
|
|
493
|
+
p === "/favicon.svg" ||
|
|
494
|
+
p === "/icon.ico" ||
|
|
495
|
+
p === "/manifest.webmanifest") {
|
|
496
|
+
res.status(404).end();
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
next();
|
|
500
|
+
});
|
|
405
501
|
app.get(/^\/(?!api\/|ws).*/, (_req, res) => {
|
|
406
502
|
// Callback form: a failed stat here (npm i -g is mid-replacement of the
|
|
407
503
|
// package dir) responds 503 instead of crashing the request pipeline
|
|
@@ -936,6 +1032,7 @@ wss.on("connection", (ws) => {
|
|
|
936
1032
|
promptOverrides: msg.promptOverrides,
|
|
937
1033
|
disabledSkills: msg.disabledSkills,
|
|
938
1034
|
disabledExtensions: msg.disabledExtensions,
|
|
1035
|
+
disabledAgentTools: msg.disabledAgentTools,
|
|
939
1036
|
disabledPlugins: msg.disabledPlugins,
|
|
940
1037
|
terminalToolsEnabled: msg.terminalToolsEnabled,
|
|
941
1038
|
terminalBash: msg.terminalBash,
|
|
@@ -945,6 +1042,7 @@ wss.on("connection", (ws) => {
|
|
|
945
1042
|
goalModeEnabled: msg.goalModeEnabled,
|
|
946
1043
|
thinkingWrap: msg.thinkingWrap,
|
|
947
1044
|
toolsWrap: msg.toolsWrap,
|
|
1045
|
+
skillsFullText: msg.skillsFullText,
|
|
948
1046
|
visionBridgeEnabled: msg.visionBridgeEnabled,
|
|
949
1047
|
visionBridgeModel: msg.visionBridgeModel,
|
|
950
1048
|
visionBridgePromptMode: msg.visionBridgePromptMode,
|
|
@@ -43,14 +43,14 @@ const TODO_GUIDANCE_ZH = [
|
|
|
43
43
|
"# 内联标记工具(状态类操作请写在回答正文,不要调用工具)",
|
|
44
44
|
"- 标记语法:[[todo:new:<主题>]] 新建;[[todo:set:<id>,completed|in_progress|pending]] 状态;[[todo:remove:<id>]] 删除;[[todo:dep:<id>,blocks=<依赖id,逗号分隔>]] 设依赖。",
|
|
45
45
|
"- 状态变化全部用上面的 [[todo:...]] 内联标记表达,不会中断回答,无需等待返回。",
|
|
46
|
-
"- 想查看/list 当前任务列表时,才用 `
|
|
46
|
+
"- 想查看/list 当前任务列表时,才用 `todo_list` 工具(读操作走工具)。",
|
|
47
47
|
"- 不要编造不存在的任务 id;id 由 [[todo:new:...]] 分配,首次分配是自增整数。",
|
|
48
48
|
];
|
|
49
49
|
const TODO_GUIDANCE_EN = [
|
|
50
50
|
"# Inline marker tools (express state changes inline in your reply text — never call a tool for them)",
|
|
51
51
|
"- Marker syntax: [[todo:new:<subject>]] to create; [[todo:set:<id>,completed|in_progress|pending]] for status; [[todo:remove:<id>]] to delete; [[todo:dep:<id>,blocks=<dep ids, comma-separated>]] to set dependencies.",
|
|
52
52
|
"- Express all status changes with the [[todo:...]] inline markers above; they never interrupt your reply and need no waiting for a result.",
|
|
53
|
-
"- Only use the `
|
|
53
|
+
"- Only use the `todo_list` tool (the read path goes through the tool) when you want to list the current tasks.",
|
|
54
54
|
"- Never invent task ids; ids are assigned by [[todo:new:...]], starting from incrementing integers.",
|
|
55
55
|
];
|
|
56
56
|
/** 语言感知的 todo guidance(issue #91):en 用英译、zh 用中文,默认英文。 */
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const EN = [
|
|
2
|
+
"<orchestrator>",
|
|
3
|
+
"You are an orchestrator, not just an implementer. Your value is decomposition,",
|
|
4
|
+
"delegation, and quality control. Default bias: DELEGATE. Work directly only when",
|
|
5
|
+
"the task is trivially small (single file, known location, direct answer).",
|
|
6
|
+
"",
|
|
7
|
+
"## Intent gate (every message)",
|
|
8
|
+
"- Check skills FIRST: if the request matches a skill trigger, read that skill file",
|
|
9
|
+
" before classifying or acting. Skills handle their tasks better than ad-hoc work.",
|
|
10
|
+
"- Verbalize routing BEFORE acting: state the detected intent (research /",
|
|
11
|
+
" implementation / investigation / evaluation / fix / open-ended) and your approach",
|
|
12
|
+
" (e.g. explore recon first, then answer; plan then delegate).",
|
|
13
|
+
"- Implement ONLY when the current message explicitly asks for implementation",
|
|
14
|
+
" (implement/add/create/fix/change/write), the scope is concrete, and no pending",
|
|
15
|
+
" specialist result blocks you. Otherwise research/clarify and wait.",
|
|
16
|
+
"- Reclassify intent from the CURRENT message only; never auto-carry",
|
|
17
|
+
" implementation mode from prior turns.",
|
|
18
|
+
"",
|
|
19
|
+
"## Delegation protocol",
|
|
20
|
+
"Available specialist templates (subagent_spawn template=):",
|
|
21
|
+
"- oracle: architecture decisions, hard debugging, multi-system tradeoffs",
|
|
22
|
+
"- metis: pre-planning analysis when scope is unclear",
|
|
23
|
+
"- momus: review a work plan for gaps before implementing",
|
|
24
|
+
"- explore: codebase recon (where is X, which file has Y)",
|
|
25
|
+
"- librarian: external docs / library usage / open-source examples",
|
|
26
|
+
"- sisyphus-junior: well-defined single-scope implementation (already researched)",
|
|
27
|
+
"- multimodal-looker: PDFs, images, diagrams needing interpretation",
|
|
28
|
+
"- review / implement / research / scout / audit: built-in execution and review roles",
|
|
29
|
+
"Match the task domain to the template. Visual work goes to frontend-capable",
|
|
30
|
+
"execution; hard logic and architecture go to oracle; unclear scope goes to metis first.",
|
|
31
|
+
"BEFORE each delegation, declare: which template, WHY its description matches the",
|
|
32
|
+
"task domain, and the expected outcome. Then call subagent_spawn.",
|
|
33
|
+
"Delegation prompts MUST include all six sections:",
|
|
34
|
+
"1. TASK (atomic, one action) 2. EXPECTED OUTCOME (concrete deliverables +",
|
|
35
|
+
"done criteria) 3. REQUIRED TOOLS (explicit whitelist) 4. MUST DO (leave nothing",
|
|
36
|
+
"implicit) 5. MUST NOT DO (forbidden actions) 6. CONTEXT (file paths, patterns,",
|
|
37
|
+
"constraints). Vague prompts fail; if your prompt is shorter than 5 lines it is too vague.",
|
|
38
|
+
"For follow-ups continue the SAME subagent session instead of starting fresh.",
|
|
39
|
+
"After delegation ALWAYS verify: does it work, does it follow codebase patterns,",
|
|
40
|
+
"did it respect MUST DO / MUST NOT DO. Never start implementing work that a",
|
|
41
|
+
"pending oracle/momus result was asked to decide.",
|
|
42
|
+
"Anti-duplication: once explore/librarian are tasked, do NOT redo their search yourself.",
|
|
43
|
+
"",
|
|
44
|
+
"## Task management",
|
|
45
|
+
"Multi-step task (2+ steps) -> create a todo list IMMEDIATELY, in detail.",
|
|
46
|
+
"Mark exactly ONE item in_progress before starting it; mark completed IMMEDIATELY",
|
|
47
|
+
"after (never batch). If scope changes, update todos before proceeding.",
|
|
48
|
+
"No todos on non-trivial work = incomplete work.",
|
|
49
|
+
"",
|
|
50
|
+
"## Constraints",
|
|
51
|
+
"NEVER: suppress type errors (as any, @ts-ignore); commit without an explicit",
|
|
52
|
+
"request; speculate about unread code; leave code broken after failures;",
|
|
53
|
+
"use empty catch blocks; delete failing tests to pass.",
|
|
54
|
+
"Prefer existing libraries, small focused changes, and minimal bugfixes",
|
|
55
|
+
"(never refactor while fixing). Run diagnostics/build/tests on changed files",
|
|
56
|
+
"before reporting completion.",
|
|
57
|
+
"</orchestrator>",
|
|
58
|
+
].join("\n");
|
|
59
|
+
const ZH = [
|
|
60
|
+
"<orchestrator>",
|
|
61
|
+
"你是编排者(orchestrator),而不只是一个执行者。你的价值在于任务分解、",
|
|
62
|
+
"委派和质量把关。默认倾向:能委派就委派。只有任务极小(单文件、位置明确、",
|
|
63
|
+
"直接可答)时才亲自动手。",
|
|
64
|
+
"",
|
|
65
|
+
"## 意图门(每条消息先过这一关)",
|
|
66
|
+
"- 先查技能:请求命中某技能触发条件时,先用 read 读该技能文件,再分类或动手。",
|
|
67
|
+
" 技能覆盖的任务,照技能流程做比临场发挥更可靠。",
|
|
68
|
+
"- 先说路由再动手:明确本轮意图(调研 / 实现 / 排查 / 评估 / 修 bug / 开放式),",
|
|
69
|
+
" 并说出你的路线(例如先 explore 摸底再回答、先计划再委派)。",
|
|
70
|
+
"- 只有同时满足才实现:本轮消息有明确的实现动词(实现/加/创建/修/改/写)、",
|
|
71
|
+
" 范围足够具体、不依赖尚未返回的 specialist 结果。否则只做调研/澄清然后等待。",
|
|
72
|
+
"- 每轮只按当前消息重判意图,不把上一轮的“实现模式”自动带过来。",
|
|
73
|
+
"",
|
|
74
|
+
"## 委派协议",
|
|
75
|
+
"可用 specialist 模板(subagent_spawn 的 template 参数):",
|
|
76
|
+
"- oracle:架构决策、难调的 bug、多系统权衡",
|
|
77
|
+
"- metis:范围不清时先做预分析",
|
|
78
|
+
"- momus:实现前先评审计划查缺补漏",
|
|
79
|
+
"- explore:代码库侦察(X 在哪、Y 在哪个文件)",
|
|
80
|
+
"- librarian:外部文档 / 第三方库用法 / 开源实现参考",
|
|
81
|
+
"- sisyphus-junior:已调研清楚的单点实现任务",
|
|
82
|
+
"- multimodal-looker:需要解读的 PDF / 图片 / 图表",
|
|
83
|
+
"- review / implement / research / scout / audit:内置的执行与审查角色",
|
|
84
|
+
"按任务领域选模板:界面视觉走前端执行、硬逻辑与架构走 oracle、范围不清先走 metis。",
|
|
85
|
+
"每次委派前必须声明:选哪个模板、它的简介与任务哪里匹配、期望产出,然后再调 subagent_spawn。",
|
|
86
|
+
"派单词必须包含六段:1. TASK(原子目标) 2. EXPECTED OUTCOME(交付物+完成标准)",
|
|
87
|
+
"3. REQUIRED TOOLS(可用工具白名单) 4. MUST DO(要求写尽) 5. MUST NOT DO(禁区)",
|
|
88
|
+
"6. CONTEXT(文件路径、既有模式、约束)。派单词短于 5 行就是太含糊,一定失败。",
|
|
89
|
+
"追问要在同一个子代理会话里继续,不要另起炉灶。",
|
|
90
|
+
"委派后必须验证:能跑吗、符合代码库既有模式吗、遵守 MUST DO / MUST NOT DO 了吗。",
|
|
91
|
+
"oracle/momus 还没回来之前,不准先把它们要定的实现写了。",
|
|
92
|
+
"禁重复劳动:explore/librarian 已经在查的东西,自己不要再查一遍。",
|
|
93
|
+
"",
|
|
94
|
+
"## 任务管理",
|
|
95
|
+
"多步任务(2 步以上)→ 立刻建 todo 列表,越细越好。",
|
|
96
|
+
"一次只把一项标 in_progress,做完立刻标 completed(不批量)。范围变了先更新 todo 再动手。",
|
|
97
|
+
"复杂任务没有 todo = 没做完。",
|
|
98
|
+
"",
|
|
99
|
+
"## 禁区",
|
|
100
|
+
"绝不:压类型错误(as any、@ts-ignore);未经明确要求就 commit;臆测没读过的代码;",
|
|
101
|
+
"失败后留下一堆 broken 代码;空 catch;删掉失败的测试来“通过”。",
|
|
102
|
+
"优先用现成库、小而聚焦的改动、最小化修 bug(修 bug 时不顺手重构)。",
|
|
103
|
+
"汇报完成前,先对改动过的文件跑一遍诊断/构建/测试。",
|
|
104
|
+
"</orchestrator>",
|
|
105
|
+
].join("\n");
|
|
106
|
+
/** 编排指导块(按语言选;Oh-my-pi Behavior/Delegation/Task/Constraints 的原生改写版)。 */
|
|
107
|
+
export function buildOrchestratorText(lang = "en") {
|
|
108
|
+
return lang === "zh" ? ZH : EN;
|
|
109
|
+
}
|
|
@@ -140,8 +140,10 @@ function escapeXml(s) {
|
|
|
140
140
|
.replace(/"/g, """)
|
|
141
141
|
.replace(/'/g, "'");
|
|
142
142
|
}
|
|
143
|
-
/** 技能段文本(不含前导空行)。与 SDK formatSkillsForPrompt 一致。
|
|
144
|
-
|
|
143
|
+
/** 技能段文本(不含前导空行)。与 SDK formatSkillsForPrompt 一致。
|
|
144
|
+
* fullText = true 全员全文注入(oh-my-pi 式:标题 + 引用描述 + body 全文);
|
|
145
|
+
* 传技能名数组 = 只注入名单里的;content 缺失的条目回落列表行,不中断渲染。 */
|
|
146
|
+
export function buildSkillsText(skills, lang = "en", fullText = false) {
|
|
145
147
|
const visible = skills.filter((s) => !s.disableModelInvocation);
|
|
146
148
|
if (visible.length === 0)
|
|
147
149
|
return "";
|
|
@@ -153,6 +155,15 @@ export function buildSkillsText(skills, lang = "en") {
|
|
|
153
155
|
"<available_skills>",
|
|
154
156
|
];
|
|
155
157
|
for (const skill of visible) {
|
|
158
|
+
// 全文模式且有内容:oh-my-pi 注入格式(### Skill: 名 / > 描述 / 全文)。
|
|
159
|
+
const inject = fullText === true || (Array.isArray(fullText) && fullText.includes(skill.name));
|
|
160
|
+
if (inject && skill.content?.trim()) {
|
|
161
|
+
lines.push(`### Skill: ${skill.name}`);
|
|
162
|
+
if (skill.description.trim())
|
|
163
|
+
lines.push(`> ${skill.description.trim()}`);
|
|
164
|
+
lines.push("", skill.content.trim(), "");
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
156
167
|
lines.push(" <skill>");
|
|
157
168
|
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
158
169
|
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
@@ -220,7 +231,7 @@ export function resolveSectionTexts(inputs) {
|
|
|
220
231
|
terminal: inputs.terminalGuidance,
|
|
221
232
|
markers: inputs.markersGuidance,
|
|
222
233
|
context: buildContextText(inputs.contextFiles, lang),
|
|
223
|
-
skills: buildSkillsText(inputs.skills, lang),
|
|
234
|
+
skills: buildSkillsText(inputs.skills, lang, inputs.skillsFullText ?? false),
|
|
224
235
|
cwd: `Current working directory: ${cwd}`,
|
|
225
236
|
};
|
|
226
237
|
}
|