paseo-plugin-chat-view 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/LICENSE +21 -0
- package/LICENSE.compact-agent-activity +21 -0
- package/LICENSE.mermaid +21 -0
- package/README.md +428 -2
- package/README_zh.md +429 -0
- package/client/activity/Activity.test.ts +220 -0
- package/client/activity/Activity.tsx +784 -0
- package/client/activity/detail/detail.test.ts +102 -0
- package/client/activity/detail/dispatch.tsx +94 -0
- package/client/activity/detail/file.tsx +93 -0
- package/client/activity/detail/index.tsx +39 -0
- package/client/activity/detail/parts.tsx +221 -0
- package/client/activity/detail/search.tsx +59 -0
- package/client/activity/detail/terminal.tsx +53 -0
- package/client/activity/detail/texts.tsx +42 -0
- package/client/activity/disclosure.ts +49 -0
- package/client/activity/dom.test.ts +52 -0
- package/client/activity/dom.ts +138 -0
- package/client/activity/group-store.test.ts +126 -0
- package/client/activity/group-store.ts +265 -0
- package/client/activity/highlight.test.ts +116 -0
- package/client/activity/highlight.ts +418 -0
- package/client/activity/history.test.ts +602 -0
- package/client/activity/history.ts +239 -0
- package/client/activity/index.ts +74 -0
- package/client/activity/latest-marker.test.ts +41 -0
- package/client/activity/latest-marker.ts +70 -0
- package/client/activity/styles.ts +535 -0
- package/client/activity/tools/child-agent.tsx +308 -0
- package/client/activity/tools/exa.tsx +99 -0
- package/client/activity/tools/github.tsx +501 -0
- package/client/activity/tools/list.tsx +102 -0
- package/client/activity/tools/lists.test.ts +193 -0
- package/client/activity/tools/paseo/agent.tsx +384 -0
- package/client/activity/tools/paseo/browser.tsx +322 -0
- package/client/activity/tools/paseo/index.tsx +79 -0
- package/client/activity/tools/paseo/parts.tsx +133 -0
- package/client/activity/tools/paseo/provider.tsx +220 -0
- package/client/activity/tools/paseo/schedule.tsx +246 -0
- package/client/activity/tools/paseo/speak.tsx +20 -0
- package/client/activity/tools/paseo/terminal.tsx +214 -0
- package/client/activity/tools/paseo/workspace.tsx +162 -0
- package/client/activity/tools/shared.tsx +431 -0
- package/client/activity/transform.test.ts +28 -0
- package/client/activity/transform.ts +43 -0
- package/client/agent-pills.ts +56 -0
- package/client/dom.ts +75 -0
- package/client/enhance/controller.ts +111 -0
- package/client/enhance/dom.test.ts +28 -0
- package/client/enhance/dom.ts +101 -0
- package/client/enhance/index.ts +95 -0
- package/client/enhance/pill.tsx +19 -0
- package/client/enhance/state.ts +58 -0
- package/client/feature.ts +22 -0
- package/client/features.test.ts +149 -0
- package/client/features.ts +83 -0
- package/client/locale.ts +45 -0
- package/client/mermaid/diagram.tsx +438 -0
- package/client/mermaid/index.ts +27 -0
- package/client/mermaid/item.tsx +28 -0
- package/client/mermaid/markdown.tsx +217 -0
- package/client/mermaid/registration.ts +32 -0
- package/client/mermaid/toolbar.tsx +175 -0
- package/client/mermaid/viewer.tsx +314 -0
- package/client/readonly/dom.test.ts +66 -0
- package/client/readonly/dom.ts +32 -0
- package/client/readonly/index.ts +27 -0
- package/client/registration.test.ts +243 -0
- package/client/settings.tsx +438 -0
- package/client/sticky/StickyBar.tsx +144 -0
- package/client/sticky/dom.test.ts +230 -0
- package/client/sticky/dom.ts +517 -0
- package/client/sticky/index.ts +38 -0
- package/client/sticky/pill.tsx +70 -0
- package/client/timeline-pager.test.ts +121 -0
- package/client/timeline-pager.ts +123 -0
- package/index.client.tsx +44 -0
- package/index.server.ts +12 -0
- package/package.json +42 -4
- package/paseo-plugin.json +6 -0
- package/server/enhance/index.ts +63 -0
- package/server/enhance/manifest.ts +176 -0
- package/server/enhance/probe.test.ts +215 -0
- package/server/enhance/probe.ts +118 -0
- package/server/enhance/provider.ts +149 -0
- package/shared/activity/child-agent.test.ts +27 -0
- package/shared/activity/child-agent.ts +26 -0
- package/shared/activity/diff.test.ts +74 -0
- package/shared/activity/diff.ts +207 -0
- package/shared/activity/exa.test.ts +66 -0
- package/shared/activity/exa.ts +183 -0
- package/shared/activity/file-kind.test.ts +13 -0
- package/shared/activity/file-kind.ts +112 -0
- package/shared/activity/github.test.ts +56 -0
- package/shared/activity/github.ts +174 -0
- package/shared/activity/palette.test.ts +26 -0
- package/shared/activity/palette.ts +106 -0
- package/shared/activity/parse.test.ts +39 -0
- package/shared/activity/parse.ts +115 -0
- package/shared/activity/paseo-tools.test.ts +163 -0
- package/shared/activity/paseo-tools.ts +497 -0
- package/shared/activity/text.test.ts +85 -0
- package/shared/activity/text.ts +202 -0
- package/shared/activity/timeline.test.ts +26 -0
- package/shared/activity/timeline.ts +108 -0
- package/shared/activity/tool-presentation.test.ts +154 -0
- package/shared/activity/tool-presentation.ts +352 -0
- package/shared/enhance/contract.ts +105 -0
- package/shared/enhance/decide.ts +97 -0
- package/shared/enhance/enhance.test.ts +188 -0
- package/shared/enhance/failure.ts +40 -0
- package/shared/enhance/index.ts +8 -0
- package/shared/enhance/normalize.ts +16 -0
- package/shared/enhance/parse.ts +79 -0
- package/shared/enhance/policy.ts +48 -0
- package/shared/enhance/prompt.ts +134 -0
- package/shared/enhance/run.ts +84 -0
- package/shared/i18n.test.ts +81 -0
- package/shared/i18n.ts +300 -0
- package/shared/mermaid/flowchart.test.ts +169 -0
- package/shared/mermaid/flowchart.ts +271 -0
- package/shared/mermaid/layout.test.ts +305 -0
- package/shared/mermaid/layout.ts +496 -0
- package/shared/mermaid/parse.test.ts +110 -0
- package/shared/mermaid/parse.ts +193 -0
- package/shared/mermaid/segment.test.ts +55 -0
- package/shared/mermaid/segment.ts +95 -0
- package/shared/mermaid/sequence.test.ts +68 -0
- package/shared/mermaid/sequence.ts +130 -0
- package/shared/mermaid/transform.test.ts +52 -0
- package/shared/mermaid/transform.ts +45 -0
- package/shared/mermaid/zoom.test.ts +134 -0
- package/shared/mermaid/zoom.ts +114 -0
- package/shared/settings.ts +41 -0
- package/shared/sticky/history.test.ts +91 -0
- package/shared/sticky/history.ts +86 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildEnhanceMessages,
|
|
4
|
+
decideEnhancement,
|
|
5
|
+
derivePolicy,
|
|
6
|
+
ENHANCE_SYSTEM_PROMPT,
|
|
7
|
+
MAX_ENHANCED_CHARS,
|
|
8
|
+
normalizeRaw,
|
|
9
|
+
parseEnhanceOutput,
|
|
10
|
+
runEnhancement,
|
|
11
|
+
type EnhanceProvider,
|
|
12
|
+
type EnvironmentProfile,
|
|
13
|
+
} from "./index";
|
|
14
|
+
|
|
15
|
+
function profile(overrides: Partial<EnvironmentProfile> = {}): EnvironmentProfile {
|
|
16
|
+
return {
|
|
17
|
+
root: "/repo",
|
|
18
|
+
projectKind: "frontend",
|
|
19
|
+
languages: ["typescript"],
|
|
20
|
+
packageManagers: ["pnpm"],
|
|
21
|
+
frameworks: ["react"],
|
|
22
|
+
commands: { build: "pnpm build", test: "pnpm test" },
|
|
23
|
+
conventions: ["测试与源码同目录"],
|
|
24
|
+
constraints: ["Node >= 20"],
|
|
25
|
+
hotspots: [{ path: "shared/enhance", reason: "本次改动最相关" }],
|
|
26
|
+
references: [{ path: "README.md", excerpt: "插件通过公开 SDK 接入" }],
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const completion = (body: string): EnhanceProvider => ({ complete: vi.fn(async () => body) });
|
|
32
|
+
|
|
33
|
+
const wrapped = (enhanced: string, delta = "补齐了验收标准", open = "无") =>
|
|
34
|
+
`<enhanced_prompt>${enhanced}</enhanced_prompt>` +
|
|
35
|
+
`<delta_summary>${delta}</delta_summary>` +
|
|
36
|
+
`<open_questions>${open}</open_questions>`;
|
|
37
|
+
|
|
38
|
+
it("normalizes cosmetic noise while keeping markdown hard breaks", () => {
|
|
39
|
+
expect(normalizeRaw(" 修复 登录\r\n\r\n\r\n结束")).toBe("修复 登录\n\n结束");
|
|
40
|
+
expect(normalizeRaw(" a \n b ")).toBe("a \n b");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("asks for expansion on a vague one-liner and keeps an already structured prompt", () => {
|
|
44
|
+
const vague = decideEnhancement("加个缓存");
|
|
45
|
+
expect(vague.enhance).toBe(true);
|
|
46
|
+
expect(vague.missing).toContain("acceptance");
|
|
47
|
+
const structured = decideEnhancement(
|
|
48
|
+
"在 shared/enhance/policy.ts 实现 monorepo 目标包映射,必须保持现有导出签名不变,验收运行 pnpm test",
|
|
49
|
+
);
|
|
50
|
+
expect(structured.enhance).toBe(false);
|
|
51
|
+
expect(structured.present).toContain("acceptance");
|
|
52
|
+
expect(decideEnhancement("ok").enhance).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("derives environment focus and never invents a command the probe did not find", () => {
|
|
56
|
+
const frontend = derivePolicy(profile());
|
|
57
|
+
expect(frontend.focus.join()).toContain("交互状态");
|
|
58
|
+
expect(frontend.acceptance).toContain("`pnpm test`");
|
|
59
|
+
expect(frontend.constraints.join()).toContain("不引入新的 UI 依赖");
|
|
60
|
+
const library = derivePolicy(profile({ projectKind: "library" }));
|
|
61
|
+
expect(library.constraints.join()).toContain("不得破坏已导出的公开签名");
|
|
62
|
+
const bare = derivePolicy(profile({ projectKind: "unknown", commands: {} }));
|
|
63
|
+
expect(bare.focus).toEqual([]);
|
|
64
|
+
expect(bare.acceptance).toContain("人工核对");
|
|
65
|
+
expect(bare.acceptance).not.toContain("`");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("keeps the system prompt byte-identical across differing environments", () => {
|
|
69
|
+
const first = buildEnhanceMessages({
|
|
70
|
+
raw: "a",
|
|
71
|
+
missing: [],
|
|
72
|
+
profile: profile(),
|
|
73
|
+
policy: derivePolicy(profile()),
|
|
74
|
+
});
|
|
75
|
+
const second = buildEnhanceMessages({
|
|
76
|
+
raw: "b",
|
|
77
|
+
missing: ["验收标准"],
|
|
78
|
+
profile: profile({ projectKind: "backend", root: "/other" }),
|
|
79
|
+
policy: derivePolicy(profile({ projectKind: "backend" })),
|
|
80
|
+
});
|
|
81
|
+
expect(first[0]).toEqual({ role: "system", content: ENHANCE_SYSTEM_PROMPT });
|
|
82
|
+
expect(second[0]).toEqual(first[0]);
|
|
83
|
+
expect(first[1]!.content).toContain("kind: frontend");
|
|
84
|
+
expect(second[1]!.content).toContain("kind: backend");
|
|
85
|
+
expect(second[1]!.content).toContain("验收标准");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("parses a well-formed completion into an applied result", () => {
|
|
89
|
+
const result = parseEnhanceOutput(
|
|
90
|
+
wrapped("增强后的正文", "补了范围", "- 是否兼容旧版"),
|
|
91
|
+
"原文",
|
|
92
|
+
[],
|
|
93
|
+
"zh-CN",
|
|
94
|
+
);
|
|
95
|
+
expect(result).toMatchObject({ applied: true, degraded: false, enhanced: "增强后的正文" });
|
|
96
|
+
expect(result.openQuestions).toEqual(["是否兼容旧版"]);
|
|
97
|
+
expect(parseEnhanceOutput(wrapped("x"), "x", [], "zh-CN")).toMatchObject({
|
|
98
|
+
applied: false,
|
|
99
|
+
degraded: false,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("degrades instead of throwing when the model misbehaves", () => {
|
|
104
|
+
expect(parseEnhanceOutput("没有标签的输出", "原文", ["验收标准"], "zh-CN")).toMatchObject({
|
|
105
|
+
enhanced: "原文",
|
|
106
|
+
applied: false,
|
|
107
|
+
degraded: true,
|
|
108
|
+
openQuestions: ["验收标准"],
|
|
109
|
+
});
|
|
110
|
+
expect(parseEnhanceOutput(wrapped("x", "x", "无"), "x", [], "zh-CN").openQuestions).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("caps the body at one flat ceiling regardless of input length", () => {
|
|
114
|
+
// The cap must not track the input: a 4-character prompt is the case that most needs expanding,
|
|
115
|
+
// and scaling the ceiling with the input is what used to return a 12-character stub.
|
|
116
|
+
expect(parseEnhanceOutput(wrapped("长".repeat(900)), "原文", [], "zh-CN").enhanced.length).toBe(
|
|
117
|
+
MAX_ENHANCED_CHARS,
|
|
118
|
+
);
|
|
119
|
+
// A body within the ceiling is delivered whole and carries no truncation notice.
|
|
120
|
+
const whole = parseEnhanceOutput(wrapped("长".repeat(120)), "原文", [], "zh-CN");
|
|
121
|
+
expect(whole.enhanced.length).toBe(120);
|
|
122
|
+
expect(whole.truncated).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("flags a shortened body so the client can say so", () => {
|
|
126
|
+
const result = parseEnhanceOutput(wrapped("长".repeat(900), "补了范围"), "原文", [], "zh-CN");
|
|
127
|
+
expect(result.truncated).toBe(true);
|
|
128
|
+
// The notice is client-side wording in the reader's language, so the summary stays the model's.
|
|
129
|
+
expect(result.deltaSummary).toBe("补了范围");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("leaves a prompt longer than the ceiling alone rather than returning less than was written", () => {
|
|
133
|
+
const long = "长".repeat(MAX_ENHANCED_CHARS + 40);
|
|
134
|
+
const decision = decideEnhancement(long);
|
|
135
|
+
expect(decision.enhance).toBe(false);
|
|
136
|
+
expect(decision.skip).toBe("tooLong");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("runs the pipeline and swallows provider failures into a degraded result", async () => {
|
|
140
|
+
const provider = completion(wrapped("完整提示词:目标、范围、约束、验收"));
|
|
141
|
+
const applied = await runEnhancement({ raw: "加个缓存", profile: profile(), provider });
|
|
142
|
+
expect(applied.applied).toBe(true);
|
|
143
|
+
expect(provider.complete).toHaveBeenCalledOnce();
|
|
144
|
+
|
|
145
|
+
const failing: EnhanceProvider = {
|
|
146
|
+
complete: vi.fn(async () => {
|
|
147
|
+
throw new Error("连接超时");
|
|
148
|
+
}),
|
|
149
|
+
};
|
|
150
|
+
const degraded = await runEnhancement({ raw: "加个缓存", profile: profile(), provider: failing });
|
|
151
|
+
expect(degraded).toMatchObject({ enhanced: "加个缓存", applied: false, degraded: true });
|
|
152
|
+
expect(degraded.degradedReason).toContain("连接超时");
|
|
153
|
+
expect(degraded.openQuestions.length).toBeGreaterThan(0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("words its reasons in the caller's language", async () => {
|
|
157
|
+
const provider = completion(wrapped("不应被调用"));
|
|
158
|
+
const skipped = await runEnhancement({
|
|
159
|
+
raw: "ok",
|
|
160
|
+
profile: profile(),
|
|
161
|
+
provider,
|
|
162
|
+
locale: "en-US",
|
|
163
|
+
});
|
|
164
|
+
expect(skipped.degradedReason).toBe("The prompt is too short to expand");
|
|
165
|
+
const failing: EnhanceProvider = {
|
|
166
|
+
complete: vi.fn(async () => {
|
|
167
|
+
throw new Error("boom");
|
|
168
|
+
}),
|
|
169
|
+
};
|
|
170
|
+
const degraded = await runEnhancement({
|
|
171
|
+
raw: "加个缓存",
|
|
172
|
+
profile: profile(),
|
|
173
|
+
provider: failing,
|
|
174
|
+
locale: "en-US",
|
|
175
|
+
});
|
|
176
|
+
expect(degraded.degradedReason).toBe("Model call failed: boom");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("skips the model entirely when the raw input is already executable", async () => {
|
|
180
|
+
const provider = completion(wrapped("不应被调用"));
|
|
181
|
+
const result = await runEnhancement({
|
|
182
|
+
raw: "在 shared/enhance/policy.ts 实现 monorepo 目标包映射,必须保持现有导出签名不变,验收运行 pnpm test",
|
|
183
|
+
profile: profile(),
|
|
184
|
+
provider,
|
|
185
|
+
});
|
|
186
|
+
expect(result.applied).toBe(false);
|
|
187
|
+
expect(provider.complete).not.toHaveBeenCalled();
|
|
188
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { t, type SupportedLocale } from "../i18n";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Why a completion request failed, as data rather than prose. The provider throws this without
|
|
5
|
+
* knowing who is reading, and the text is rendered where the language is known — which is also
|
|
6
|
+
* what lets the connection probe classify a failure without pattern-matching localized sentences.
|
|
7
|
+
*/
|
|
8
|
+
export type ProviderFailureKind = "http" | "timeout" | "truncatedOutput" | "emptyResponse";
|
|
9
|
+
|
|
10
|
+
export class ProviderFailure extends Error {
|
|
11
|
+
readonly kind: ProviderFailureKind;
|
|
12
|
+
/** The raw detail: the endpoint's own body, or the timeout in milliseconds as a string. */
|
|
13
|
+
readonly detail: string;
|
|
14
|
+
readonly status?: number;
|
|
15
|
+
|
|
16
|
+
constructor(kind: ProviderFailureKind, detail: string, status?: number) {
|
|
17
|
+
super(detail ? `${kind}: ${detail}` : kind);
|
|
18
|
+
this.name = "ProviderFailure";
|
|
19
|
+
this.kind = kind;
|
|
20
|
+
this.detail = detail;
|
|
21
|
+
this.status = status;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The one place a provider failure becomes a sentence. */
|
|
26
|
+
export function providerFailureMessage(locale: SupportedLocale, failure: ProviderFailure): string {
|
|
27
|
+
switch (failure.kind) {
|
|
28
|
+
case "http":
|
|
29
|
+
return t(locale, "enhance.failure.http", {
|
|
30
|
+
status: failure.status ?? "?",
|
|
31
|
+
detail: failure.detail,
|
|
32
|
+
});
|
|
33
|
+
case "timeout":
|
|
34
|
+
return t(locale, "enhance.failure.timeout", { ms: failure.detail });
|
|
35
|
+
case "truncatedOutput":
|
|
36
|
+
return t(locale, "enhance.failure.truncatedOutput");
|
|
37
|
+
case "emptyResponse":
|
|
38
|
+
return t(locale, "enhance.failure.emptyResponse");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/g;
|
|
2
|
+
const FULL_WIDTH_SPACE = /\u3000/g;
|
|
3
|
+
const EXCESS_BLANK_LINES = /\n{3,}/g;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Collapse cosmetic noise without rewriting wording. Trailing spaces are kept because two of
|
|
7
|
+
* them are a hard line break in Markdown.
|
|
8
|
+
*/
|
|
9
|
+
export function normalizeRaw(raw: string): string {
|
|
10
|
+
return raw
|
|
11
|
+
.replace(ZERO_WIDTH, "")
|
|
12
|
+
.replace(FULL_WIDTH_SPACE, " ")
|
|
13
|
+
.replace(/\r\n?/g, "\n")
|
|
14
|
+
.replace(EXCESS_BLANK_LINES, "\n\n")
|
|
15
|
+
.trim();
|
|
16
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { t, type SupportedLocale } from "../i18n";
|
|
2
|
+
import { MAX_ENHANCED_CHARS, type EnhanceResult } from "./contract";
|
|
3
|
+
import { normalizeRaw } from "./normalize";
|
|
4
|
+
|
|
5
|
+
const ENHANCED_BLOCK = /<enhanced_prompt>([\s\S]*?)<\/enhanced_prompt>/i;
|
|
6
|
+
const DELTA_BLOCK = /<delta_summary>([\s\S]*?)<\/delta_summary>/i;
|
|
7
|
+
const OPEN_BLOCK = /<open_questions>([\s\S]*?)<\/open_questions>/i;
|
|
8
|
+
const NOTHING = /^(无|none|n\/a|无。|-)$/i;
|
|
9
|
+
|
|
10
|
+
/** A failed enhancement must still land something usable in the composer, never an exception. */
|
|
11
|
+
export function degradedResult(
|
|
12
|
+
original: string,
|
|
13
|
+
reason: string,
|
|
14
|
+
missing: string[] = [],
|
|
15
|
+
): EnhanceResult {
|
|
16
|
+
return {
|
|
17
|
+
enhanced: original,
|
|
18
|
+
deltaSummary: "",
|
|
19
|
+
openQuestions: missing,
|
|
20
|
+
applied: false,
|
|
21
|
+
degraded: true,
|
|
22
|
+
degradedReason: reason,
|
|
23
|
+
truncated: false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The pre-check already decided the raw input is executable; keep it byte-identical. */
|
|
28
|
+
export function unchangedResult(original: string, reason: string): EnhanceResult {
|
|
29
|
+
return {
|
|
30
|
+
enhanced: original,
|
|
31
|
+
deltaSummary: "",
|
|
32
|
+
openQuestions: [],
|
|
33
|
+
applied: false,
|
|
34
|
+
degraded: false,
|
|
35
|
+
degradedReason: reason,
|
|
36
|
+
truncated: false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* `locale` only reaches the reason strings; the delivered text is the model's own. Callers that
|
|
42
|
+
* have a language to show must pass it, or a failure reads as Chinese to an English reader.
|
|
43
|
+
*/
|
|
44
|
+
export function parseEnhanceOutput(
|
|
45
|
+
raw: string,
|
|
46
|
+
original: string,
|
|
47
|
+
missing: string[],
|
|
48
|
+
locale: SupportedLocale,
|
|
49
|
+
): EnhanceResult {
|
|
50
|
+
const enhanced = ENHANCED_BLOCK.exec(raw)?.[1]?.trim() ?? "";
|
|
51
|
+
if (!enhanced) return degradedResult(original, t(locale, "enhance.reason.noBody"), missing);
|
|
52
|
+
const limit = MAX_ENHANCED_CHARS;
|
|
53
|
+
const truncated = enhanced.length > limit;
|
|
54
|
+
const body = truncated ? truncate(enhanced, limit) : enhanced;
|
|
55
|
+
if (body === normalizeRaw(original))
|
|
56
|
+
return unchangedResult(original, t(locale, "enhance.reason.equivalent"));
|
|
57
|
+
return {
|
|
58
|
+
enhanced: body,
|
|
59
|
+
deltaSummary: DELTA_BLOCK.exec(raw)?.[1]?.trim() ?? "",
|
|
60
|
+
openQuestions: parseOpenQuestions(OPEN_BLOCK.exec(raw)?.[1] ?? ""),
|
|
61
|
+
applied: true,
|
|
62
|
+
degraded: false,
|
|
63
|
+
degradedReason: "",
|
|
64
|
+
truncated,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseOpenQuestions(block: string): string[] {
|
|
69
|
+
return block
|
|
70
|
+
.split("\n")
|
|
71
|
+
.map((line) => line.replace(/^\s*[-*\d.、]+\s*/, "").trim())
|
|
72
|
+
.filter((line) => line.length > 0 && !NOTHING.test(line));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function truncate(text: string, limit: number): string {
|
|
76
|
+
const kept = text.slice(0, limit);
|
|
77
|
+
const lastOpen = kept.lastIndexOf("<");
|
|
78
|
+
return lastOpen > limit - 40 ? kept.slice(0, lastOpen) : kept;
|
|
79
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { EnvironmentProfile, ProjectKind } from "./contract";
|
|
2
|
+
|
|
3
|
+
export interface EnhancePolicy {
|
|
4
|
+
/** Environment-specific dimensions appended to the four required ones. */
|
|
5
|
+
focus: string[];
|
|
6
|
+
/** Acceptance guidance limited to commands the probe actually found. */
|
|
7
|
+
acceptance: string;
|
|
8
|
+
/** Environment-specific hard constraints passed through verbatim. */
|
|
9
|
+
constraints: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const KIND_FOCUS: Record<ProjectKind, readonly string[]> = {
|
|
13
|
+
frontend: ["交互状态(空/加载/错误/边界)", "响应式断点", "可访问性", "视觉一致性"],
|
|
14
|
+
backend: ["输入校验与错误码", "幂等性", "数据迁移与向后兼容", "并发与超时"],
|
|
15
|
+
cli: ["命令与参数接口", "退出码", "--help 文案", "破坏性变更的确认提示"],
|
|
16
|
+
library: ["公开 API 稳定性与导出面", "副作用边界", "可运行示例"],
|
|
17
|
+
monorepo: ["明确目标包与路径", "跨包改动的影响面", "构建顺序与依赖约束"],
|
|
18
|
+
docs: ["目标读者与阅读路径", "章节结构与锚点", "示例可执行性"],
|
|
19
|
+
unknown: [],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const KIND_CONSTRAINTS: Record<ProjectKind, readonly string[]> = {
|
|
23
|
+
frontend: ["沿用现有组件库与样式体系,不引入新的 UI 依赖"],
|
|
24
|
+
backend: ["接口变更需保持向后兼容,或同时给出迁移方案"],
|
|
25
|
+
cli: ["不得静默改变既有退出码与参数解析"],
|
|
26
|
+
library: ["不得破坏已导出的公开签名"],
|
|
27
|
+
monorepo: ["变更需限定到具体包,避免影响其他包的构建产物"],
|
|
28
|
+
docs: [],
|
|
29
|
+
unknown: [],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const COMMAND_ORDER = ["build", "typecheck", "lint", "test"] as const;
|
|
33
|
+
|
|
34
|
+
/** Read-only projection of the profile; the model receives evidence, not conclusions. */
|
|
35
|
+
export function derivePolicy(profile: EnvironmentProfile): EnhancePolicy {
|
|
36
|
+
const focus = [...KIND_FOCUS[profile.projectKind]];
|
|
37
|
+
const constraints = [...KIND_CONSTRAINTS[profile.projectKind], ...profile.constraints];
|
|
38
|
+
const found = COMMAND_ORDER.filter((name) => profile.commands[name]).map(
|
|
39
|
+
(name) => `${name}: \`${profile.commands[name]}\``,
|
|
40
|
+
);
|
|
41
|
+
return {
|
|
42
|
+
focus,
|
|
43
|
+
acceptance: found.length
|
|
44
|
+
? `验收只能引用这些已存在的命令:${found.join("、")}`
|
|
45
|
+
: "仓库未发现标准验证命令,验收标准改写为可人工核对的现象",
|
|
46
|
+
constraints,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { EnvironmentProfile } from "./contract";
|
|
2
|
+
import type { EnhancePolicy } from "./policy";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Byte-stable on purpose: every volatile detail (raw input, environment, policy) lives in the
|
|
6
|
+
* user message, so provider-side prefix caching keeps hitting across projects and runs.
|
|
7
|
+
* Edit the text and bump RULE_VERSION together — tests assert on rule ids, not on prose.
|
|
8
|
+
*/
|
|
9
|
+
export const ENHANCE_RULE_VERSION = 3;
|
|
10
|
+
|
|
11
|
+
export const ENHANCE_SYSTEM_PROMPT = `你是一个提示词增强器。你不是编程助手,不回答用户的任务本身,只产出指令。
|
|
12
|
+
|
|
13
|
+
# 唯一职责
|
|
14
|
+
把 <raw> 改写为一份可以直接交给编码 AI 执行、无需追问即可开工的提示词。
|
|
15
|
+
|
|
16
|
+
# 输入契约
|
|
17
|
+
<raw> 用户原始诉求
|
|
18
|
+
<missing> 原始诉求尚未覆盖的维度
|
|
19
|
+
<context> 项目环境画像(可能为空,或 kind 为 unknown)
|
|
20
|
+
<policy> 由环境推导出的补充维度、验收限制与硬约束
|
|
21
|
+
缺失即未知,禁止假设;禁止编造文件路径、命令、依赖、版本。
|
|
22
|
+
|
|
23
|
+
# 判定先行
|
|
24
|
+
依据 <missing> 判断:
|
|
25
|
+
- 缺 goal(未说明要做什么动作),或缺 2 项以上维度:执行增强。
|
|
26
|
+
- 四个维度基本齐备:原样返回 raw,禁止同义改写造句。
|
|
27
|
+
|
|
28
|
+
# 四个必填维度
|
|
29
|
+
每项一行,冒号后直接给内容,不加解释、不举例:
|
|
30
|
+
1 goal:目标与成功判据。
|
|
31
|
+
2 scope:范围与边界,明确不做什么。
|
|
32
|
+
3 constraints:约束,含技术栈、版本、性能、兼容、不可破坏项。
|
|
33
|
+
4 acceptance:验收与验证方式。
|
|
34
|
+
另按 <policy> 的 focus 补充环境相关维度;acceptance 只能引用 policy 限定的命令。
|
|
35
|
+
|
|
36
|
+
# 环境适配规则
|
|
37
|
+
- 必须使用 context 中出现的真实技术栈名与真实命令,不得替换为"你使用的框架"或"常用命令"。
|
|
38
|
+
- 验收只能引用 policy.acceptance 列出的命令;不存在验证命令时不得编造命令名。
|
|
39
|
+
- context 中的 constraints 视为硬约束,必须原样保留。
|
|
40
|
+
- 优先用 context 的 hotspots 路径锚定范围。
|
|
41
|
+
- context 的 kind 为 unknown 时不做环境偏置,并把"技术栈未知"列入 open_questions。
|
|
42
|
+
- references 是项目自己的文档,与 context 一致;冲突时以 context 为准。
|
|
43
|
+
|
|
44
|
+
# 硬约束
|
|
45
|
+
不改变用户原意;不引入用户未要求的目标;不输出解决方案代码,只输出提示词正文;
|
|
46
|
+
未知处写 {{待确认}} 并列入 open_questions;语言跟随用户原文语言;
|
|
47
|
+
正文不超过 200 字。宁可少写一句,也要把四个维度都写全——写满 200 字但缺维度,等于没写。
|
|
48
|
+
|
|
49
|
+
# 输出格式
|
|
50
|
+
只输出以下结构,不加任何解释:
|
|
51
|
+
<enhanced_prompt>增强后的提示词正文</enhanced_prompt>
|
|
52
|
+
<delta_summary>一句话说明补了什么,50 字以内</delta_summary>
|
|
53
|
+
<open_questions>每行一条待确认项,没有就写"无"</open_questions>
|
|
54
|
+
|
|
55
|
+
# 静默自检
|
|
56
|
+
输出前自查,不把自查过程写进产物:
|
|
57
|
+
是否存在凭空事实?是否混入用户没要求的目标?能否独立执行无需追问?是否与 context 冲突?
|
|
58
|
+
任一不过则重写后再输出。`;
|
|
59
|
+
|
|
60
|
+
export interface EnhanceMessage {
|
|
61
|
+
role: "system" | "user";
|
|
62
|
+
content: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface EnhancePromptInput {
|
|
66
|
+
raw: string;
|
|
67
|
+
missing: string[];
|
|
68
|
+
profile: EnvironmentProfile;
|
|
69
|
+
policy: EnhancePolicy;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function buildEnhanceMessages(input: EnhancePromptInput): EnhanceMessage[] {
|
|
73
|
+
return [
|
|
74
|
+
{ role: "system", content: ENHANCE_SYSTEM_PROMPT },
|
|
75
|
+
{ role: "user", content: renderUserBlock(input) },
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function renderUserBlock(input: EnhancePromptInput): string {
|
|
80
|
+
return [
|
|
81
|
+
"<raw>",
|
|
82
|
+
input.raw,
|
|
83
|
+
"</raw>",
|
|
84
|
+
"",
|
|
85
|
+
"<missing>",
|
|
86
|
+
input.missing.length ? input.missing.join("、") : "无",
|
|
87
|
+
"</missing>",
|
|
88
|
+
"",
|
|
89
|
+
"<context>",
|
|
90
|
+
renderProfile(input.profile),
|
|
91
|
+
"</context>",
|
|
92
|
+
"",
|
|
93
|
+
"<policy>",
|
|
94
|
+
`focus: ${input.policy.focus.length ? input.policy.focus.join(";") : "无"}`,
|
|
95
|
+
`acceptance: ${input.policy.acceptance}`,
|
|
96
|
+
`constraints: ${input.policy.constraints.length ? input.policy.constraints.join(";") : "无"}`,
|
|
97
|
+
"</policy>",
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Only fields the probe actually filled; an absent fact is reported as absent, not as a guess. */
|
|
102
|
+
function renderProfile(profile: EnvironmentProfile): string {
|
|
103
|
+
const lines: string[] = [`kind: ${profile.projectKind}`, `root: ${profile.root}`];
|
|
104
|
+
pushList(lines, "languages", profile.languages);
|
|
105
|
+
pushList(lines, "packageManagers", profile.packageManagers);
|
|
106
|
+
pushList(lines, "frameworks", profile.frameworks);
|
|
107
|
+
const commands: string[] = [];
|
|
108
|
+
for (const [name, command] of Object.entries(profile.commands))
|
|
109
|
+
if (command) commands.push(`${name}=${command}`);
|
|
110
|
+
if (commands.length) lines.push(`commands: ${commands.join("; ")}`);
|
|
111
|
+
pushList(lines, "conventions", profile.conventions);
|
|
112
|
+
pushList(lines, "constraints", profile.constraints);
|
|
113
|
+
if (profile.hotspots.length)
|
|
114
|
+
lines.push(
|
|
115
|
+
`hotspots: ${profile.hotspots.map((hotspot) => `${hotspot.path} (${hotspot.reason})`).join("; ")}`,
|
|
116
|
+
);
|
|
117
|
+
if (profile.references.length) {
|
|
118
|
+
lines.push("references:");
|
|
119
|
+
for (const reference of profile.references)
|
|
120
|
+
lines.push(`- ${reference.path}\n${indent(reference.excerpt, " ")}`);
|
|
121
|
+
}
|
|
122
|
+
return lines.join("\n");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function pushList(lines: string[], label: string, values: readonly string[]) {
|
|
126
|
+
if (values.length) lines.push(`${label}: ${values.join(", ")}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function indent(text: string, prefix: string): string {
|
|
130
|
+
return text
|
|
131
|
+
.split("\n")
|
|
132
|
+
.map((line) => `${prefix}${line}`)
|
|
133
|
+
.join("\n");
|
|
134
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { t, type SupportedLocale } from "../i18n";
|
|
2
|
+
import { MAX_ENHANCED_CHARS, type EnhanceResult, type EnvironmentProfile } from "./contract";
|
|
3
|
+
import {
|
|
4
|
+
DIMENSION_KEYS,
|
|
5
|
+
DIMENSION_LABELS,
|
|
6
|
+
decideEnhancement,
|
|
7
|
+
type EnhanceDecision,
|
|
8
|
+
} from "./decide";
|
|
9
|
+
import { providerFailureMessage, ProviderFailure } from "./failure";
|
|
10
|
+
import { normalizeRaw } from "./normalize";
|
|
11
|
+
import { derivePolicy } from "./policy";
|
|
12
|
+
import { degradedResult, parseEnhanceOutput, unchangedResult } from "./parse";
|
|
13
|
+
import { buildEnhanceMessages, type EnhanceMessage } from "./prompt";
|
|
14
|
+
|
|
15
|
+
/** The single seam the outside world plugs into: swap this and the core stays untouched. */
|
|
16
|
+
export interface EnhanceProvider {
|
|
17
|
+
complete(messages: EnhanceMessage[]): Promise<string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface EnhanceRunInput {
|
|
21
|
+
raw: string;
|
|
22
|
+
profile: EnvironmentProfile;
|
|
23
|
+
provider: EnhanceProvider;
|
|
24
|
+
/** The reader's language; only the reasons depend on it. Defaults to Chinese. */
|
|
25
|
+
locale?: SupportedLocale;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function runEnhancement(input: EnhanceRunInput): Promise<EnhanceResult> {
|
|
29
|
+
const locale = input.locale ?? "zh-CN";
|
|
30
|
+
const original = normalizeRaw(input.raw);
|
|
31
|
+
const decision = decideEnhancement(original);
|
|
32
|
+
const missing = decision.missing.map((dimension) => DIMENSION_LABELS[dimension]);
|
|
33
|
+
if (!decision.enhance) return unchangedResult(original, describeSkip(locale, decision));
|
|
34
|
+
const policy = derivePolicy(input.profile);
|
|
35
|
+
let completion: string;
|
|
36
|
+
try {
|
|
37
|
+
const messages = buildEnhanceMessages({
|
|
38
|
+
raw: original,
|
|
39
|
+
missing,
|
|
40
|
+
profile: input.profile,
|
|
41
|
+
policy,
|
|
42
|
+
});
|
|
43
|
+
completion = await input.provider.complete(messages);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return degradedResult(original, describeFailure(locale, error), missing);
|
|
46
|
+
}
|
|
47
|
+
return parseEnhanceOutput(completion, original, missing, locale);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The pre-check's verdict, worded for the reader: which dimensions it found and which it missed. */
|
|
51
|
+
function describeSkip(locale: SupportedLocale, decision: EnhanceDecision): string {
|
|
52
|
+
switch (decision.skip) {
|
|
53
|
+
case "tooShort":
|
|
54
|
+
return t(locale, "enhance.skip.tooShort");
|
|
55
|
+
case "alreadyStructured":
|
|
56
|
+
return t(locale, "enhance.skip.alreadyStructured");
|
|
57
|
+
case "missingDimensions":
|
|
58
|
+
return t(locale, "enhance.skip.missingDimensions", {
|
|
59
|
+
items: decision.missing
|
|
60
|
+
.map((dimension) => t(locale, DIMENSION_KEYS[dimension]))
|
|
61
|
+
.join(locale === "zh-CN" ? "、" : ", "),
|
|
62
|
+
});
|
|
63
|
+
case "tooLong":
|
|
64
|
+
return t(locale, "enhance.skip.tooLong", { limit: MAX_ENHANCED_CHARS });
|
|
65
|
+
default:
|
|
66
|
+
return t(locale, "enhance.unchanged");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function describeFailure(locale: SupportedLocale, error: unknown): string {
|
|
71
|
+
if (error instanceof ProviderFailure) return providerFailureMessage(locale, error);
|
|
72
|
+
return t(locale, "enhance.reason.providerFailed", { detail: describeError(error) });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Missing credentials are a configuration problem, not a model failure; keep the raw text. */
|
|
76
|
+
export function notConfiguredResult(raw: string, locale: SupportedLocale): EnhanceResult {
|
|
77
|
+
const original = normalizeRaw(raw);
|
|
78
|
+
return degradedResult(original, t(locale, "enhance.reason.notConfigured"));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function describeError(error: unknown): string {
|
|
82
|
+
if (error instanceof Error) return error.message;
|
|
83
|
+
return String(error);
|
|
84
|
+
}
|