dsh-plan-completion 0.0.0-stage → 0.1.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 +46 -0
- package/LICENSE +21 -0
- package/README.md +262 -2
- package/client.js +1995 -0
- package/cordis.patch.yml +12 -0
- package/docs/delivery-notes.md +104 -0
- package/docs/design-v1.2.md +387 -0
- package/docs/experiment-archive.md +36 -0
- package/docs/phase-2-plan.md +69 -0
- package/docs/publish-assessment.md +99 -0
- package/docs/screenshots/always-on-hint.png +0 -0
- package/docs/screenshots/dark-theme.png +0 -0
- package/docs/screenshots/light-theme.png +0 -0
- package/docs/screenshots/plan-panel.png +0 -0
- package/host/README.md +443 -0
- package/host/config.js +87 -0
- package/host/evidence.js +392 -0
- package/host/experiment.js +261 -0
- package/host/heuristics.js +160 -0
- package/host/job.js +905 -0
- package/host/plan.js +613 -0
- package/host/prompt.js +386 -0
- package/host/store.js +312 -0
- package/host/text.js +168 -0
- package/icon.svg +12 -0
- package/index.js +1326 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +56 -4
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* host/heuristics.js — 启发式建议与降级计划骨架(纯函数,零依赖)。
|
|
3
|
+
*
|
|
4
|
+
* 设计依据:v1.2 §3.5.4(模型不可用 → 启发式)、§3.6(降级链:
|
|
5
|
+
* 「计划下一步 / 失败工具 / 未完成 todo」;无模型 → 首条消息骨架 + todo)。
|
|
6
|
+
*
|
|
7
|
+
* 优先级(与 task 验收一致):
|
|
8
|
+
* 1. 失败工具 → 修报错
|
|
9
|
+
* 2. 有未完成 todo/doing → 下一个
|
|
10
|
+
* 3. 最后一条 assistant 是提问 → 给倾向性决策
|
|
11
|
+
* 4. 全部 done/dropped → 验收
|
|
12
|
+
* 5. 否则 → 计划下一步 title / 按 intent 起步
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { DEFAULTS } from './config.js';
|
|
16
|
+
import { statusFor } from './plan.js';
|
|
17
|
+
import { clampChars, looksLikeQuestion, oneLine, sanitizeSuggestion } from './text.js';
|
|
18
|
+
|
|
19
|
+
const isObj = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
20
|
+
|
|
21
|
+
function make(text, kind) {
|
|
22
|
+
const clean = sanitizeSuggestion(text, DEFAULTS.suggestionMaxChars);
|
|
23
|
+
if (!clean.ok) return { ok: false, text: '', kind: 'none' };
|
|
24
|
+
return { ok: true, text: clean.text, kind };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function firstFailedTool(evidence) {
|
|
28
|
+
const failed = Array.isArray(evidence?.failedTools) ? evidence.failedTools.filter((tool) => isObj(tool)) : [];
|
|
29
|
+
if (failed.length) return failed[failed.length - 1];
|
|
30
|
+
const calls = Array.isArray(evidence?.toolCalls) ? evidence.toolCalls : [];
|
|
31
|
+
const fromCalls = calls.filter((tool) => isObj(tool) && tool.ok === false);
|
|
32
|
+
return fromCalls.length ? fromCalls[fromCalls.length - 1] : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function nextOpenStep(plan) {
|
|
36
|
+
if (!isObj(plan) || !Array.isArray(plan.steps)) return null;
|
|
37
|
+
const doing = plan.steps.find((step) => isObj(step) && step.status === 'doing');
|
|
38
|
+
if (doing) return doing;
|
|
39
|
+
const todo = plan.steps.find((step) => isObj(step) && step.status === 'todo');
|
|
40
|
+
return todo ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 生成启发式建议(模型不可用/超时/非法输出时的兜底)。
|
|
45
|
+
*
|
|
46
|
+
* @param {object} evidence collectFromEvents 的输出(也可传任意同名字段对象)
|
|
47
|
+
* @returns {{ok:true, text:string, kind:string}|{ok:false, text:'', kind:'none'}}
|
|
48
|
+
*/
|
|
49
|
+
export function heuristicSuggestion(evidence = {}) {
|
|
50
|
+
try {
|
|
51
|
+
const source = isObj(evidence) ? evidence : {};
|
|
52
|
+
const plan = isObj(source.plan) ? source.plan : null;
|
|
53
|
+
|
|
54
|
+
// 1. 失败工具 → 修报错
|
|
55
|
+
const failed = firstFailedTool(source);
|
|
56
|
+
if (failed) {
|
|
57
|
+
const name = clampChars(oneLine(failed.name) || '工具', 14);
|
|
58
|
+
const error = clampChars(oneLine(failed.error), 20);
|
|
59
|
+
if (error) return make(`先修 ${name} 的报错:${error}`, 'fix-error');
|
|
60
|
+
return make(`先修 ${name} 的报错再继续下一步`, 'fix-error');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2. 有未完成步骤 → 下一个
|
|
64
|
+
const next = nextOpenStep(plan);
|
|
65
|
+
if (next) {
|
|
66
|
+
const title = clampChars(oneLine(next.title), 24) || '下一步';
|
|
67
|
+
return make(`接着做「${title}」,先跑通再补细节`, 'next-step');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 3. 提问回合 → 给倾向性决策
|
|
71
|
+
if (source.hasQuestion === true || looksLikeQuestion(source.lastAssistantText)) {
|
|
72
|
+
return make('按方案 A 继续,先把最小可用版本跑通', 'decide');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 4. 全部完成 → 验收
|
|
76
|
+
if (plan && statusFor(plan.steps) === 'completed') {
|
|
77
|
+
return make('功能已完成,先自测验收再收尾', 'accept');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 5. 否则 → 计划下一步 title / intent 起步
|
|
81
|
+
if (plan && Array.isArray(plan.steps) && plan.steps.length) {
|
|
82
|
+
const last = plan.steps[plan.steps.length - 1];
|
|
83
|
+
const title = clampChars(oneLine(last?.title), 22) || clampChars(oneLine(plan.intent), 22);
|
|
84
|
+
if (title) return make(`先收尾「${title}」并确认结果`, 'next-title');
|
|
85
|
+
}
|
|
86
|
+
const intent = clampChars(
|
|
87
|
+
oneLine(plan?.intent) || oneLine(source.intent) || oneLine(source.firstUserMessage) || '',
|
|
88
|
+
22,
|
|
89
|
+
);
|
|
90
|
+
if (intent) return make(`先摸清现状,把「${intent}」拆成 3 步开始`, 'intent');
|
|
91
|
+
return { ok: false, text: '', kind: 'none' };
|
|
92
|
+
} catch {
|
|
93
|
+
return { ok: false, text: '', kind: 'none' };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 骨架模板:按 intent 关键词选择 3~5 个里程碑。 */
|
|
98
|
+
const SKELETONS = [
|
|
99
|
+
{
|
|
100
|
+
match: /(bug|报错|错误|修复|崩溃|异常|失败|fix|error|crash)/i,
|
|
101
|
+
titles: ['复现并定位报错根因', '做最小修复并验证', '补回归测试防止复发'],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
match: /(测试|用例|回归|覆盖率|test|spec)/i,
|
|
105
|
+
titles: ['梳理测试范围与现状', '补关键路径测试用例', '跑通回归并修复问题'],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
match: /(设计|方案|架构|重构|优化|选型)/i,
|
|
109
|
+
titles: ['确认目标、约束与验收标准', '出 2 个方案并对比取舍', '落地最小可行版本', '验证效果并收尾'],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
match: /(文档|readme|说明|注释|doc)/i,
|
|
113
|
+
titles: ['梳理要写的文档范围', '补写核心文档与示例', '校对并验证示例可运行'],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
match: /(接口|api|联调|集成|部署|发布)/i,
|
|
117
|
+
titles: ['确认接口契约与环境', '实现并打通主流程', '联调异常路径', '验收并发布'],
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 降级计划骨架:无模型时按首条消息生成 3~5 步(交给 createSeedPlan 落版本 0.0)。
|
|
123
|
+
*
|
|
124
|
+
* @param {string} [intent] 用户目标(首条消息 / 现计划 intent)
|
|
125
|
+
* @returns {{ok:true, intent:string, steps:Array<{id:string,title:string,status:'todo',source:'model'}>}|{ok:false, code:string, error:string}}
|
|
126
|
+
*/
|
|
127
|
+
export function fallbackPlan(intent) {
|
|
128
|
+
try {
|
|
129
|
+
const goal = clampChars(oneLine(intent), 24);
|
|
130
|
+
let template = SKELETONS[0];
|
|
131
|
+
if (goal) {
|
|
132
|
+
for (const item of SKELETONS) {
|
|
133
|
+
if (item.match.test(goal)) {
|
|
134
|
+
template = item;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (template === SKELETONS[0] && !SKELETONS[0].match.test(goal)) template = null;
|
|
139
|
+
} else {
|
|
140
|
+
template = null;
|
|
141
|
+
}
|
|
142
|
+
let titles = template ? [...template.titles] : ['摸清现状与约束', '实现核心改动', '自测并修复问题', '补测试与文档'];
|
|
143
|
+
if (goal) {
|
|
144
|
+
// 把目标自然嵌进第一/第二步,让骨架与用户意图相关。
|
|
145
|
+
titles = titles.map((title, index) => (index === 0 && !title.includes(goal) ? `${title}:${goal}` : title));
|
|
146
|
+
}
|
|
147
|
+
const steps = titles.map((title, index) => ({
|
|
148
|
+
id: `s${index + 1}`,
|
|
149
|
+
title: clampChars(title, DEFAULTS.maxTitleChars),
|
|
150
|
+
status: 'todo',
|
|
151
|
+
source: 'model',
|
|
152
|
+
}));
|
|
153
|
+
if (steps.length < DEFAULTS.minSteps || steps.length > DEFAULTS.maxSteps) {
|
|
154
|
+
return { ok: false, code: 'bad-skeleton', error: '骨架步骤数不在 3~5 之间' };
|
|
155
|
+
}
|
|
156
|
+
return { ok: true, intent: clampChars(oneLine(intent), DEFAULTS.maxIntentChars), steps };
|
|
157
|
+
} catch (err) {
|
|
158
|
+
return { ok: false, code: 'internal', error: `骨架生成异常:${err && err.message ? err.message : String(err)}` };
|
|
159
|
+
}
|
|
160
|
+
}
|