opencode-prompt-expand 0.0.0-stage → 0.1.1
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 +18 -0
- package/LICENSE +21 -0
- package/README.md +338 -2
- package/assets/bmc-qr.png +0 -0
- package/assets/featured.html +260 -0
- package/assets/featured.png +0 -0
- package/dist/config.js +76 -0
- package/dist/gui-experimental.cjs +61 -0
- package/dist/index.js +170 -0
- package/dist/model.js +104 -0
- package/dist/optimizer.js +86 -0
- package/dist/project-context.js +121 -0
- package/dist/prompts.js +109 -0
- package/dist/rpc.js +47 -0
- package/dist/types.js +1 -0
- package/docs/README.zh-CN.md +177 -0
- package/package.json +64 -4
package/dist/config.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { DEFAULT_PROMPTS } from "./prompts.js";
|
|
2
|
+
export const DEFAULT_SKIP_PATTERNS = [
|
|
3
|
+
/^\s*(git|npm|pnpm|yarn|bun|docker|kubectl|ls|cd|cat|curl)\b/i,
|
|
4
|
+
/^\s*[\/!]/,
|
|
5
|
+
/^(继续|好的|收到|谢谢|可以|ok|okay|yes|no|go|continue|thanks?)[。.!!]?$/i,
|
|
6
|
+
/^\s*[\d\W]{1,3}\s*$/,
|
|
7
|
+
];
|
|
8
|
+
export const DEFAULT_CONFIG = {
|
|
9
|
+
model: "auto",
|
|
10
|
+
mode: "concise",
|
|
11
|
+
maxRounds: 5,
|
|
12
|
+
timeoutMs: 30000,
|
|
13
|
+
maxGrowth: 5,
|
|
14
|
+
minChars: 10,
|
|
15
|
+
maxPromptChars: 6000,
|
|
16
|
+
skipPatterns: DEFAULT_SKIP_PATTERNS,
|
|
17
|
+
skipOnMentions: true,
|
|
18
|
+
projectContext: true,
|
|
19
|
+
projectContextBudget: 2048,
|
|
20
|
+
draftFallback: true,
|
|
21
|
+
prompts: DEFAULT_PROMPTS,
|
|
22
|
+
};
|
|
23
|
+
function numberOption(value, fallback, min) {
|
|
24
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
25
|
+
return fallback;
|
|
26
|
+
return Math.max(min, Math.floor(value));
|
|
27
|
+
}
|
|
28
|
+
function booleanOption(value, fallback) {
|
|
29
|
+
return typeof value === "boolean" ? value : fallback;
|
|
30
|
+
}
|
|
31
|
+
function compilePatterns(value) {
|
|
32
|
+
if (!Array.isArray(value))
|
|
33
|
+
return DEFAULT_SKIP_PATTERNS;
|
|
34
|
+
const patterns = [];
|
|
35
|
+
for (const entry of value) {
|
|
36
|
+
if (typeof entry !== "string")
|
|
37
|
+
continue;
|
|
38
|
+
try {
|
|
39
|
+
patterns.push(new RegExp(entry, "i"));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Invalid user pattern: ignore it rather than breaking plugin setup.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return patterns;
|
|
46
|
+
}
|
|
47
|
+
function mergePrompts(value) {
|
|
48
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
49
|
+
return DEFAULT_PROMPTS;
|
|
50
|
+
const merged = { ...DEFAULT_PROMPTS };
|
|
51
|
+
for (const [key, prompt] of Object.entries(value)) {
|
|
52
|
+
if (typeof prompt === "string" && prompt.trim())
|
|
53
|
+
merged[key] = prompt;
|
|
54
|
+
}
|
|
55
|
+
return merged;
|
|
56
|
+
}
|
|
57
|
+
export function resolveConfig(options = {}, overrides = {}) {
|
|
58
|
+
const mode = overrides.mode ?? options.mode;
|
|
59
|
+
const model = overrides.model ?? options.model;
|
|
60
|
+
const projectContext = overrides.projectContext ?? options.projectContext;
|
|
61
|
+
return {
|
|
62
|
+
model: typeof model === "string" && model.trim() ? model.trim() : DEFAULT_CONFIG.model,
|
|
63
|
+
mode: mode === "full" ? "full" : "concise",
|
|
64
|
+
maxRounds: numberOption(options.maxRounds, DEFAULT_CONFIG.maxRounds, 1),
|
|
65
|
+
timeoutMs: numberOption(options.timeoutMs, DEFAULT_CONFIG.timeoutMs, 500),
|
|
66
|
+
maxGrowth: numberOption(options.maxGrowth, DEFAULT_CONFIG.maxGrowth, 1),
|
|
67
|
+
minChars: numberOption(options.minChars, DEFAULT_CONFIG.minChars, 1),
|
|
68
|
+
maxPromptChars: numberOption(options.maxPromptChars, DEFAULT_CONFIG.maxPromptChars, 1),
|
|
69
|
+
skipPatterns: compilePatterns(options.skipPatterns),
|
|
70
|
+
skipOnMentions: booleanOption(options.skipOnMentions, DEFAULT_CONFIG.skipOnMentions),
|
|
71
|
+
projectContext: booleanOption(projectContext, DEFAULT_CONFIG.projectContext),
|
|
72
|
+
projectContextBudget: numberOption(options.projectContextBudget, DEFAULT_CONFIG.projectContextBudget, 0),
|
|
73
|
+
draftFallback: booleanOption(options.draftFallback, DEFAULT_CONFIG.draftFallback),
|
|
74
|
+
prompts: mergePrompts(options.prompts),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const i=require("solid-js/web"),m=require("@opencode/plugin/gui"),W=require("solid-js"),K=require("solid-js/store"),J=require("@opencode/plugin/rpc"),b=require("effect");function Y(e,t){e.add(m.Command,()=>({id:"expand",get title(){return e.t("command.expand")},get description(){return e.t("command.expand.desc")},keywords:"prompt expand enhance rewrite improve",section:"prompt",slash:{name:"expand",arguments:!0},run:n=>{t.expand(n)}})),e.add(m.Command,()=>({id:"regenerate",get title(){return e.t("command.regenerate")},get description(){return e.t("command.regenerate.desc")},keywords:"prompt regenerate rewrite another version",section:"prompt",run:()=>{t.regenerate()}})),e.add(m.Command,()=>({id:"restore",get title(){return e.t("command.restore")},get description(){return e.t("command.restore.desc")},keywords:"prompt restore original undo",section:"prompt",run:()=>t.restore()})),e.add(m.Command,()=>({id:"previous",get title(){return e.t("command.previous")},keywords:"prompt version previous back",section:"prompt",run:()=>t.previous()})),e.add(m.Command,()=>({id:"next",get title(){return e.t("command.next")},keywords:"prompt version next forward",section:"prompt",run:()=>t.next()})),e.add(m.Command,()=>({id:"mode",get title(){return e.t("command.mode")},keywords:"prompt mode concise full",section:"prompt",run:()=>t.toggleMode()})),e.add(m.Command,()=>({id:"settings",get title(){return e.t("command.settings")},keywords:"prompt expand settings options",section:"prompt",run:()=>e.layout.settings("prompt-expand")}))}const Q=`你是提示词增强器。输入是用户的原始请求,输出是增强后的提示词。
|
|
2
|
+
|
|
3
|
+
【最高优先级】
|
|
4
|
+
1. 保持原目标:不改变用户要做的事,不缩窄、不替换、不增加新任务。
|
|
5
|
+
2. 不引入用户未提及的技术栈、框架、版本、文件路径、接口名。不把猜测写成事实。
|
|
6
|
+
3. 只写"做什么",不写"怎么做";不解释实现步骤,不索要教程或代码示例。
|
|
7
|
+
4. 不执行用户的请求,只把它扩写得更具体。
|
|
8
|
+
5. 语言与输入一致;只输出增强后的提示词,不要任何解释、前缀或代码块围栏。
|
|
9
|
+
|
|
10
|
+
【增强要求】
|
|
11
|
+
- 主动补齐:任务范围、约束条件、期望产出、验收方式、输出结构。
|
|
12
|
+
- 原提示已经很清楚时,做轻度润色,不要原样返回。
|
|
13
|
+
- 完整且简洁;不要以未完成的列表、悬空的连词或冒号结尾。
|
|
14
|
+
- 不添加无关要求、没有依据的事实、多余的章节。
|
|
15
|
+
- 去除口语冗余与重复。
|
|
16
|
+
|
|
17
|
+
【改写结构】(按需使用,不必每项都写)
|
|
18
|
+
- 目标:一句话说清要达成什么
|
|
19
|
+
- 范围:做什么、不做什么
|
|
20
|
+
- 上下文:用户已给出的约束与材料
|
|
21
|
+
- 要求:编号的可执行项
|
|
22
|
+
- 产出:格式、结构、长度
|
|
23
|
+
- 验收:怎样算完成`,X=`你是提示词增强器。输入是用户的原始请求,输出是增强后的提示词。
|
|
24
|
+
|
|
25
|
+
【最高优先级】
|
|
26
|
+
1. 保持原目标:不改变用户要做的事,不缩窄、不替换、不增加新任务。
|
|
27
|
+
2. 不引入用户未提及的技术栈、框架、版本、文件路径、接口名。不把猜测写成事实。
|
|
28
|
+
3. 只写"做什么",不写"怎么做";不解释实现步骤,不索要教程或代码示例。
|
|
29
|
+
4. 不执行用户的请求,只把它扩写得更具体。
|
|
30
|
+
5. 语言与输入一致;只输出增强后的提示词,不要任何解释、前缀或代码块围栏。
|
|
31
|
+
|
|
32
|
+
【增强要求】
|
|
33
|
+
- 主动补齐:任务范围、约束条件、期望产出、验收方式、输出结构。
|
|
34
|
+
- 原提示已经很清楚时,做轻度润色,不要原样返回。
|
|
35
|
+
- 完整且简洁;不要以未完成的列表、悬空的连词或冒号结尾。
|
|
36
|
+
- 不添加无关要求、没有依据的事实、多余的章节。
|
|
37
|
+
- 去除口语冗余与重复。
|
|
38
|
+
|
|
39
|
+
【面向 GPT 的结构偏好】
|
|
40
|
+
- 用编号列表写清可执行要求,步骤顺序明确。
|
|
41
|
+
- 每条验收标准写成可判定的条件,避免"更好""高端"这类主观词。
|
|
42
|
+
- 产出格式写清楚(文件、表格、清单或代码块),需要时给出字段名。`,Z=`你是提示词增强器。输入是用户的原始请求,输出是增强后的提示词。
|
|
43
|
+
|
|
44
|
+
【最高优先级】
|
|
45
|
+
1. 保持原目标:不改变用户要做的事,不缩窄、不替换、不增加新任务。
|
|
46
|
+
2. 不引入用户未提及的技术栈、框架、版本、文件路径、接口名。不把猜测写成事实。
|
|
47
|
+
3. 只写"做什么",不写"怎么做";不解释实现步骤,不索要教程或代码示例。
|
|
48
|
+
4. 不执行用户的请求,只把它扩写得更具体。
|
|
49
|
+
5. 语言与输入一致;只输出增强后的提示词,不要任何解释、前缀或代码块围栏。
|
|
50
|
+
|
|
51
|
+
【增强要求】
|
|
52
|
+
- 主动补齐:任务范围、约束条件、期望产出、验收方式、输出结构。
|
|
53
|
+
- 原提示已经很清楚时,做轻度润色,不要原样返回。
|
|
54
|
+
- 完整且简洁;不要以未完成的列表、悬空的连词或冒号结尾。
|
|
55
|
+
- 不添加无关要求、没有依据的事实、多余的章节。
|
|
56
|
+
- 去除口语冗余与重复。
|
|
57
|
+
|
|
58
|
+
【面向 Claude 的结构偏好】
|
|
59
|
+
- 优先写清目标、边界和约束,用连贯段落表达,不必强行机械编号。
|
|
60
|
+
- 明确说明哪些内容不在范围内,减少越界改动。
|
|
61
|
+
- 验收方式写成"完成时应满足什么",而不是步骤清单。`,V={default:Q,"*gpt*":X,"*claude*":Z},ee=[/^\s*(git|npm|pnpm|yarn|bun|docker|kubectl|ls|cd|cat|curl)\b/i,/^\s*[\/!]/,/^(继续|好的|收到|谢谢|可以|ok|okay|yes|no|go|continue|thanks?)[。.!!]?$/i,/^\s*[\d\W]{1,3}\s*$/],x={model:"auto",maxRounds:5,timeoutMs:3e4,maxGrowth:5,minChars:10,maxPromptChars:6e3,skipOnMentions:!0,projectContext:!0,projectContextBudget:2048,draftFallback:!0};function M(e,t,n){return typeof e!="number"||!Number.isFinite(e)?t:Math.max(n,Math.floor(e))}function H(e,t){return typeof e=="boolean"?e:t}function te(e){if(!Array.isArray(e))return ee;const t=[];for(const n of e)if(typeof n=="string")try{t.push(new RegExp(n,"i"))}catch{}return t}function oe(e){if(!e||typeof e!="object"||Array.isArray(e))return V;const t={...V};for(const[n,s]of Object.entries(e))typeof s=="string"&&s.trim()&&(t[n]=s);return t}function ne(e={},t={}){const n=t.mode??e.mode,s=t.model??e.model,d=t.projectContext??e.projectContext;return{model:typeof s=="string"&&s.trim()?s.trim():x.model,mode:n==="full"?"full":"concise",maxRounds:M(e.maxRounds,x.maxRounds,1),timeoutMs:M(e.timeoutMs,x.timeoutMs,500),maxGrowth:M(e.maxGrowth,x.maxGrowth,1),minChars:M(e.minChars,x.minChars,1),maxPromptChars:M(e.maxPromptChars,x.maxPromptChars,1),skipPatterns:te(e.skipPatterns),skipOnMentions:H(e.skipOnMentions,x.skipOnMentions),projectContext:H(d,x.projectContext),projectContextBudget:M(e.projectContextBudget,x.projectContextBudget,0),draftFallback:H(e.draftFallback,x.draftFallback),prompts:oe(e.prompts)}}const re=[/目标/,/范围/,/要求/,/验收/],se=/^(继续|好的|收到|谢谢|可以|ok|okay|yes|no|go|continue|thanks?)[。.!!]?$/i,ie=/(^|\s)@[\w./\\-]{2,}/;function ae(e){return ie.test(e)}function de(e){return re.filter(t=>t.test(e)).length}function ce(e){return de(e)>=3}function ue(e,t){const n=e.trim(),s=ce(n);return n?/^\s*[\/!]/.test(n)?{skip:!0,reason:"command",notify:!1,structured:s}:se.test(n)?{skip:!0,reason:"confirmation",notify:!1,structured:s}:I(n)<t.minChars?{skip:!0,reason:"too_short",notify:!0,structured:s}:I(n)>t.maxPromptChars?{skip:!0,reason:"too_long",notify:!1,structured:s}:t.skipPatterns.some(d=>d.test(n))?{skip:!0,reason:"pattern",notify:!1,structured:s}:t.skipOnMentions&&ae(n)?{skip:!0,reason:"mention",notify:!0,structured:s}:{skip:!1,notify:!1,structured:s}:{skip:!0,reason:"empty",notify:!1,structured:s}}function I(e){return Array.from(e).length}const le={type:"object",properties:{providerID:{type:"string"},modelID:{type:"string"},variant:{type:"string"}},required:["providerID","modelID"],additionalProperties:!1},pe=J.Rpc.define({id:"prompt-expand",events:{},methods:{optimize:{input:{type:"object",properties:{text:{type:"string"},mode:{enum:["concise","full"]},round:{type:"integer",minimum:1},previous:{type:"string"},sessionID:{type:"string"},directory:{type:"string"},composerModel:le,modelPreference:{type:"string"},projectContext:{type:"boolean"}},required:["text","mode","round"],additionalProperties:!1},output:{type:"object",properties:{ok:{type:"boolean"},text:{type:"string"},model:{type:"string"},code:{type:"string"},message:{type:"string"}},required:["ok"],additionalProperties:!1}}}}),S="__prompt_expand_draft__";function me(e){return e instanceof HTMLTextAreaElement?!e.disabled&&!e.readOnly:e instanceof HTMLInputElement?(e.type==="text"||e.type==="")&&!e.disabled&&!e.readOnly:e.isContentEditable}function fe(e){const t=e.getBoundingClientRect();if(t.width<=0||t.height<=0)return!1;const n=window.getComputedStyle(e);return n.visibility!=="hidden"&&n.display!=="none"}function F(){return typeof document>"u"?null:Array.from(document.querySelectorAll('textarea, input, [contenteditable="true"]')).filter(t=>me(t)&&fe(t)).map(t=>({element:t,rect:t.getBoundingClientRect()})).filter(({rect:t})=>t.bottom>window.innerHeight*.35).sort((t,n)=>n.rect.bottom-t.rect.bottom)[0]?.element??null}function ge(e){return e?e instanceof HTMLTextAreaElement||e instanceof HTMLInputElement?e.value:e.innerText??e.textContent??"":""}function he(e,t){if(!e||!e.isConnected)throw new Error("No draft composer found");if(e.focus(),e instanceof HTMLTextAreaElement||e instanceof HTMLInputElement){const d=e instanceof HTMLTextAreaElement?HTMLTextAreaElement.prototype:HTMLInputElement.prototype,u=Object.getOwnPropertyDescriptor(d,"value")?.set;u?u.call(e,t):e.value=t,e.dispatchEvent(new InputEvent("input",{bubbles:!0,inputType:"insertText",data:t})),e.setSelectionRange(t.length,t.length);return}const n=window.getSelection();if(n){const d=document.createRange();d.selectNodeContents(e),n.removeAllRanges(),n.addRange(d)}if(document.execCommand("insertText",!1,t)||(e.textContent=t,e.dispatchEvent(new InputEvent("beforeinput",{bubbles:!0,inputType:"insertText",data:t})),e.dispatchEvent(new InputEvent("input",{bubbles:!0,inputType:"insertText",data:t}))),!e.isConnected)throw new Error("Draft composer disappeared during write");e.focus()}function ye(){return{composer:{text:()=>ge(F()),setText:t=>he(F(),t),focus:()=>F()?.focus(),model:()=>{}},available:()=>F()!==null}}function ve(e){if(e.ok)return"";const t=e.code?.trim(),n=e.message?.trim(),s=[t?`[${t}]`:"",n].filter(Boolean).join(" ");return s.length>300?`${s.slice(0,297)}...`:s}class xe{runs=new Map;maxVersions;constructor(t=5){this.maxVersions=Math.max(1,t)}get(t){return this.runs.get(t)}start(t,n){const s={original:n,versions:[],index:-1};return this.runs.set(t,s),s}sync(t,n){const s=this.runs.get(t);return!s||n!==s.versions[s.index]?this.start(t,n):s}push(t,n,s){const d=this.runs.get(t)??this.start(t,n);return d.original=d.versions.length===0?n:d.original,d.versions.length>=this.maxVersions&&(d.versions.shift(),d.index>=0&&(d.index-=1)),d.versions.push(s),d.index=d.versions.length-1,d}setIndex(t,n){const s=this.runs.get(t);if(!(!s||n<0||n>=s.versions.length))return s.index=n,s}clear(t){this.runs.delete(t)}clearAll(){this.runs.clear()}}function be(e,t){const n=new xe(5),[s,d]=K.createStore({});function u(){const o=e.stores.prefs.value,a=o.model?.trim();return ne(e.options,{mode:o.mode,model:a&&a!=="auto"?a:void 0,projectContext:o.projectContext})}function p(o,a,r){e.toast.show({title:e.t(o),variant:"default"})}function l(o,a,r){const c=a?e.t(a):"",g=r?.trim().slice(0,300),h=[c,g].filter(Boolean).join(" ");e.toast.show({title:e.t(o),...h?{description:h}:{},variant:"error"})}function C(){try{const o=e.sessions.current();return o?.directory?o.directory:(e.servers.get("sidecar")??e.servers.list().map(r=>e.servers.get(r)).find(r=>r!==void 0))?.data.location.default().directory}catch{return}}function w(o){const a=e.sessions.current();if(a){if(o&&o!==a.id)return;const c=e.screen.current();if(!c){l("toast.noComposer","toast.noComposerDesc");return}return{key:a.id,composer:c.composer,directory:C()}}if(o&&o!==S)return;if(e.options.draftFallback===!1){l("toast.noComposer","toast.noComposerDesc");return}const r=e.router.path();if(r.startsWith("/settings")||r.startsWith("/plugins")){l("toast.noComposer","toast.noComposerDesc");return}if(!t.available()){l("toast.noComposer","toast.noComposerDesc");return}return{key:S,composer:t.composer,directory:C()}}function P(o,a){if(a){if(o==="too_short"){p("toast.tooShort");return}o==="mention"&&p("toast.mention")}}function E(o,a,r){const c=w(o);if(!(!c||c.key!==o)){if(c.composer.text()!==a){p("toast.edited");return}try{c.composer.setText(r),c.composer.focus(),n.clear(o),e.toast.show({title:e.t("toast.restored"),variant:"success"})}catch{l("toast.failed","toast.failedDesc")}}}async function v(o,a){const r=w(a);if(!r||s[r.key])return;const c=u(),g=typeof o=="string"&&o.trim().length>0,h=g?o.trim():r.composer.text();if(!h.trim())return;const _=ue(h,c);if(_.skip){P(_.reason,_.notify);return}const y=g?n.start(r.key,h):n.sync(r.key,r.composer.text());let D=1,A;if(!g&&y.versions.length>0&&r.composer.text()===y.versions[y.index]&&(D=y.versions.length+1,A=y.versions[y.index]),D>c.maxRounds){p("toast.limit");return}d(r.key,!0);try{const f=await e.client.rpc(pe).optimize({text:h,mode:c.mode,round:D,...A?{previous:A}:{},...r.key!==S?{sessionID:r.key}:{},...r.directory?{directory:r.directory}:{},...r.composer.model()?{composerModel:r.composer.model()}:{},modelPreference:c.model,projectContext:c.projectContext});if(!f.ok){const q=ve(f);console.error("[prompt-expand] optimize failed:",q),f.code==="no_model"?l("toast.noModel","toast.noModelDesc",q):l("toast.failed","toast.failedDesc",q);return}r.composer.setText(f.text),r.composer.focus(),n.push(r.key,y.original,f.text);const T=I(y.original),B=I(f.text);e.toast.show({title:e.t("toast.expanded"),description:`${T} → ${B} ${e.t("toast.chars")}`,variant:"success",duration:1e4,actions:[{label:e.t("action.undo"),run:()=>E(r.key,f.text,y.original)},{label:e.t("action.regenerate"),run:()=>{v(void 0,r.key)}}]})}catch(f){const T=f instanceof Error?f.message:String(f);console.error("[prompt-expand] expand failed:",T),l("toast.failed","toast.failedDesc",T)}finally{d(r.key,!1)}}function O(o){return v(void 0,o)}function j(o){const a=w(o);if(!a)return;const r=n.get(a.key);if(!r||r.versions.length===0){p("toast.noVersions");return}if(a.composer.text()!==r.versions[r.index]){p("toast.edited");return}try{a.composer.setText(r.original),a.composer.focus(),n.clear(a.key),e.toast.show({title:e.t("toast.restored"),variant:"success"})}catch{l("toast.failed","toast.failedDesc")}}function k(o,a){const r=w(a);if(!r)return;const c=n.sync(r.key,r.composer.text());if(c.versions.length===0){p("toast.noVersions");return}const g=c.index+o;if(g<0||g>=c.versions.length){p("toast.noVersions");return}const h=c.versions[g];try{r.composer.setText(h),r.composer.focus(),n.setIndex(r.key,g)}catch{l("toast.failed","toast.failedDesc")}}function $(){const o=e.options.mode==="full"?"full":"concise",r=(e.stores.prefs.value.mode??o)==="concise"?"full":"concise";e.stores.prefs.update(c=>{c.mode=r}),p(r==="concise"?"toast.modeConcise":"toast.modeFull")}function R(o){return s[o]===!0}function L(o){return R(o)?{label:e.t("pill.expanding"),tone:"success"}:{label:e.t("pill.expand"),run:()=>{v(void 0,o)}}}return{expand:v,regenerate:O,restore:j,previous:o=>k(-1,o),next:o=>k(1,o),toggleMode:$,busy:R,pill:L}}const Ce=b.Schema.Struct({mode:b.Schema.optional(b.Schema.Union([b.Schema.Literal("concise"),b.Schema.Literal("full")])),model:b.Schema.optional(b.Schema.String),projectContext:b.Schema.optional(b.Schema.Boolean)}),ke=m.Plugin.define({i18n:{en:{"command.expand":"Expand prompt","command.expand.desc":"Turn the current draft into a structured prompt.","command.regenerate":"Regenerate prompt","command.regenerate.desc":"Generate the next expanded version of the current draft.","command.restore":"Restore original prompt","command.restore.desc":"Restore the draft from before the first expansion.","command.previous":"Previous expanded version","command.next":"Next expanded version","command.mode":"Toggle expansion mode","command.settings":"Prompt Expand settings","pill.expand":"⚡ Expand Prompt","pill.expanding":"Expanding…","toast.expanded":"Expanded","toast.chars":"chars","toast.failed":"Expand failed","toast.failedDesc":"Original text was not changed.","toast.tooShort":"Too short to expand","toast.mention":"Contains @ references, skipped","toast.noComposer":"No composer found","toast.noComposerDesc":"Put the cursor in the composer and try again.","toast.noModel":"No connected model","toast.noModelDesc":"Connect a provider or choose a model first.","toast.limit":"5-version limit reached","toast.edited":"Text was edited, not restored","toast.noVersions":"No expanded versions yet","toast.restored":"Original restored","toast.modeConcise":"Mode: concise","toast.modeFull":"Mode: full","action.undo":"Undo","action.regenerate":"Regenerate","settings.title":"Prompt Expand","settings.description":"Configure how drafts are expanded.","settings.mode":"Mode","settings.mode.concise":"Concise (~800 chars)","settings.mode.full":"Full expansion","settings.model":"Model","settings.model.help":"auto, composer, or provider/model; empty uses opencode.jsonc","settings.projectContext":"Project context","settings.projectContext.help":"Read package.json, rule files, and git status.","settings.note":"No default keyboard shortcut. Bind commands in Settings > Shortcuts."},zh:{"command.expand":"扩展提示词","command.expand.desc":"把当前草稿扩展成结构化提示词。","command.regenerate":"重新生成提示词","command.regenerate.desc":"为当前草稿生成下一版扩展结果。","command.restore":"恢复原始提示词","command.restore.desc":"恢复到第一次扩展前的草稿。","command.previous":"上一版扩展结果","command.next":"下一版扩展结果","command.mode":"切换扩展模式","command.settings":"提示词扩展设置","pill.expand":"⚡ Expand Prompt","pill.expanding":"正在扩展…","toast.expanded":"已扩展","toast.chars":"字符","toast.failed":"扩展失败","toast.failedDesc":"原文未改动。","toast.tooShort":"太短,无需扩展","toast.mention":"含 @ 引用,已跳过","toast.noComposer":"未找到输入框","toast.noComposerDesc":"请把光标放进 composer 后重试。","toast.noModel":"没有可用模型","toast.noModelDesc":"请先连接 provider 或选择一个模型。","toast.limit":"已达 5 版上限","toast.edited":"内容已修改,未恢复","toast.noVersions":"还没有扩展版本","toast.restored":"已恢复原文","toast.modeConcise":"模式:简洁","toast.modeFull":"模式:充分展开","action.undo":"撤销","action.regenerate":"重生成","settings.title":"提示词扩展","settings.description":"配置草稿扩展方式。","settings.mode":"模式","settings.mode.concise":"简洁(约 800 字符)","settings.mode.full":"充分展开","settings.model":"模型","settings.model.help":"auto、composer 或 provider/model;留空使用 opencode.jsonc","settings.projectContext":"项目感知","settings.projectContext.help":"读取 package.json、规则文件和 git 状态。","settings.note":"默认没有快捷键,可在 Settings > Shortcuts 中绑定。"}},stores:{prefs:m.Store.global(Ce,{})}});function Se(e,t){return{id:"expand",show:n=>t.pill(n.session.id)}}var we=i.template('<div style="padding:8px 0"><h2 style=font-size:16px;margin-bottom:8px></h2><p></p><div><span></span><select><option value=concise></option><option value=full></option></select></div><div><span></span><input><span></span></div><div><label style=display:flex;align-items:center;gap:8px><input type=checkbox></label><span></span></div><p>');const z={display:"flex","flex-direction":"column",gap:"6px","margin-bottom":"20px","max-width":"560px"},U={"font-size":"13px","font-weight":"600"},N={"font-size":"12px",opacity:"0.65"},G={"font-size":"13px",padding:"6px 8px","border-radius":"6px",border:"1px solid rgba(127, 127, 127, 0.35)",background:"transparent",color:"inherit"};function Te(){const e=m.usePlugin(),t=e.stores.prefs;return(()=>{var n=we(),s=n.firstChild,d=s.nextSibling,u=d.nextSibling,p=u.firstChild,l=p.nextSibling,C=l.firstChild,w=C.nextSibling,P=u.nextSibling,E=P.firstChild,v=E.nextSibling,O=v.nextSibling,j=P.nextSibling,k=j.firstChild,$=k.firstChild,R=k.nextSibling,L=j.nextSibling;return i.insert(s,()=>e.t("settings.title")),i.insert(d,()=>e.t("settings.description")),i.insert(p,()=>e.t("settings.mode")),l.addEventListener("change",o=>{const a=o.currentTarget.value==="full"?"full":"concise";t.update(r=>{r.mode=a})}),i.insert(C,()=>e.t("settings.mode.concise")),i.insert(w,()=>e.t("settings.mode.full")),i.insert(E,()=>e.t("settings.model")),v.$$input=o=>{const a=o.currentTarget.value;t.update(r=>{r.model=a})},i.setAttribute(v,"spellcheck",!1),i.insert(O,()=>e.t("settings.model.help")),$.addEventListener("change",o=>{const a=o.currentTarget.checked;t.update(r=>{r.projectContext=a})}),i.insert(k,()=>e.t("settings.projectContext"),null),i.insert(R,()=>e.t("settings.projectContext.help")),i.insert(L,()=>e.t("settings.note")),i.effect(o=>{var a=N,r=z,c=U,g=G,h=z,_=U,y=G,D=N,A=z,f={...U},T=N,B=N;return o.e=i.style(d,a,o.e),o.t=i.style(u,r,o.t),o.a=i.style(p,c,o.a),o.o=i.style(l,g,o.o),o.i=i.style(P,h,o.i),o.n=i.style(E,_,o.n),o.s=i.style(v,y,o.s),o.h=i.style(O,D,o.h),o.r=i.style(j,A,o.r),o.d=i.style(k,f,o.d),o.l=i.style(R,T,o.l),o.u=i.style(L,B,o.u),o},{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0,n:void 0,s:void 0,h:void 0,r:void 0,d:void 0,l:void 0,u:void 0}),i.effect(()=>l.value=t.value.mode??"concise"),i.effect(()=>v.value=t.value.model??""),i.effect(()=>$.checked=t.value.projectContext??!0),n})()}function Me(e){return{id:"prompt-expand",get title(){return e.t("settings.title")},get entries(){return[{id:"prompt-expand",title:e.t("settings.title"),description:e.t("settings.description")}]},render:()=>i.createComponent(Te,{})}}i.delegateEvents(["input"]);var Pe=i.template('<div style="display:flex;justify-content:center;padding:6px 0"><button type=button style="font-size:13px;padding:5px 12px;border-radius:999px;border:1px solid rgba(127, 127, 127, 0.35);background:transparent;color:inherit">');const Ee=e=>{const t=ye(),n=be(e,t);Y(e,n),e.add(m.ComposerPill,Se(e,n)),e.add(m.SettingsPage,Me(e)),e.add(m.Slot,{at:"window.bottom",render:()=>i.createComponent(W.Show,{get when(){return i.memo(()=>!e.screen.current()&&!e.router.path().startsWith("/settings"))()&&!e.router.path().startsWith("/plugins")},get children(){var s=Pe(),d=s.firstChild;return d.$$click=()=>{n.expand()},i.insert(d,(()=>{var u=i.memo(()=>!!n.busy(S));return()=>u()?e.t("pill.expanding"):e.t("pill.expand")})()),i.effect(u=>{var p=n.busy(S),l=n.busy(S)?"default":"pointer",C=n.busy(S)?.65:1;return p!==u.e&&(d.disabled=u.e=p),l!==u.t&&i.setStyleProperty(d,"cursor",u.t=l),C!==u.a&&i.setStyleProperty(d,"opacity",u.a=C),u},{e:void 0,t:void 0,a:void 0}),s}})})},je=m.Plugin.define({...ke,setup:Ee});i.delegateEvents(["click"]);exports.default=je;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Plugin } from "@opencode/plugin";
|
|
2
|
+
import { resolveConfig } from "./config.js";
|
|
3
|
+
import { buildPrompt } from "./prompts.js";
|
|
4
|
+
import { cleanOutput, shouldSkip, validateOutput } from "./optimizer.js";
|
|
5
|
+
import { pickModel } from "./model.js";
|
|
6
|
+
import { readProjectContext, resolveProjectDirectory } from "./project-context.js";
|
|
7
|
+
import { PromptExpand } from "./rpc.js";
|
|
8
|
+
class TimeoutError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super("timeout");
|
|
11
|
+
this.name = "TimeoutError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function withTimeout(promise, timeoutMs) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const timer = setTimeout(() => reject(new TimeoutError()), timeoutMs);
|
|
17
|
+
promise.then((value) => {
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
resolve(value);
|
|
20
|
+
}, (error) => {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
reject(error);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async function readVcs(ctx, directory) {
|
|
27
|
+
try {
|
|
28
|
+
const input = { location: { directory } };
|
|
29
|
+
const info = await ctx.vcs.get(input);
|
|
30
|
+
const status = await ctx.vcs.status(input);
|
|
31
|
+
return {
|
|
32
|
+
branch: info?.data?.branch?.current,
|
|
33
|
+
files: Array.isArray(status?.data) ? status.data.map((file) => file.file) : [],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function modelRefFromSession(session) {
|
|
41
|
+
const model = session?.model;
|
|
42
|
+
if (!model?.providerID || !model?.id)
|
|
43
|
+
return undefined;
|
|
44
|
+
return {
|
|
45
|
+
providerID: model.providerID,
|
|
46
|
+
id: model.id,
|
|
47
|
+
...(model.variant ? { variant: model.variant } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export default Plugin.define({
|
|
51
|
+
id: "prompt-expand",
|
|
52
|
+
async setup(ctx) {
|
|
53
|
+
const registration = await ctx.rpc.register(PromptExpand, {
|
|
54
|
+
optimize: async (raw, rpcContext) => {
|
|
55
|
+
const input = raw;
|
|
56
|
+
const config = resolveConfig(ctx.options, {
|
|
57
|
+
mode: input.mode,
|
|
58
|
+
model: input.modelPreference,
|
|
59
|
+
projectContext: input.projectContext,
|
|
60
|
+
});
|
|
61
|
+
const decision = shouldSkip(input.text, config);
|
|
62
|
+
if (decision.skip) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
code: decision.reason ?? "skipped",
|
|
66
|
+
message: decision.reason ?? "skipped",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
let session;
|
|
70
|
+
try {
|
|
71
|
+
if (input.sessionID)
|
|
72
|
+
session = await ctx.session.get({ sessionID: input.sessionID });
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
session = undefined;
|
|
76
|
+
}
|
|
77
|
+
let models = [];
|
|
78
|
+
let defaultModel = null;
|
|
79
|
+
try {
|
|
80
|
+
const listed = (await ctx.model.list());
|
|
81
|
+
models = Array.isArray(listed) ? listed : Array.isArray(listed?.data) ? listed.data : [];
|
|
82
|
+
const selected = (await ctx.model.default());
|
|
83
|
+
defaultModel =
|
|
84
|
+
selected && "data" in selected
|
|
85
|
+
? (selected.data ?? null)
|
|
86
|
+
: selected;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
models = [];
|
|
90
|
+
defaultModel = null;
|
|
91
|
+
}
|
|
92
|
+
const sessionModel = modelRefFromSession(session);
|
|
93
|
+
const picked = pickModel({
|
|
94
|
+
preference: config.model,
|
|
95
|
+
composerModel: input.composerModel,
|
|
96
|
+
sessionModel,
|
|
97
|
+
models,
|
|
98
|
+
defaultModel,
|
|
99
|
+
});
|
|
100
|
+
if (!picked) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
code: "no_model",
|
|
104
|
+
message: "No connected chat model is available.",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
let projectContext = "";
|
|
108
|
+
if (config.projectContext) {
|
|
109
|
+
const directory = resolveProjectDirectory({
|
|
110
|
+
sessionDirectory: session?.location?.directory,
|
|
111
|
+
requestDirectory: input.directory,
|
|
112
|
+
pluginDirectory: ctx.location?.directory,
|
|
113
|
+
});
|
|
114
|
+
if (directory) {
|
|
115
|
+
const vcs = await readVcs(ctx, directory);
|
|
116
|
+
try {
|
|
117
|
+
projectContext = await readProjectContext(directory, config.projectContextBudget, vcs);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
projectContext = "";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const targetModelID = input.composerModel?.modelID ?? sessionModel?.id;
|
|
125
|
+
const prompt = buildPrompt({
|
|
126
|
+
text: input.text,
|
|
127
|
+
mode: config.mode,
|
|
128
|
+
structured: decision.structured,
|
|
129
|
+
previous: input.previous,
|
|
130
|
+
round: input.round,
|
|
131
|
+
projectContext,
|
|
132
|
+
targetModelID,
|
|
133
|
+
prompts: config.prompts,
|
|
134
|
+
});
|
|
135
|
+
try {
|
|
136
|
+
const generated = await withTimeout(ctx.experimental.generate.text({
|
|
137
|
+
model: picked.model,
|
|
138
|
+
prompt,
|
|
139
|
+
}), config.timeoutMs);
|
|
140
|
+
const cleaned = cleanOutput(generated.text);
|
|
141
|
+
const validation = validateOutput(cleaned, input.text, config);
|
|
142
|
+
if (!validation.ok) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
code: `invalid_${validation.reason}`,
|
|
146
|
+
message: "The generated prompt did not pass validation.",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
ok: true,
|
|
151
|
+
text: cleaned,
|
|
152
|
+
model: `${picked.model.providerID}/${picked.model.id}`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (rpcContext?.signal?.aborted) {
|
|
157
|
+
return { ok: false, code: "aborted", message: "Request aborted." };
|
|
158
|
+
}
|
|
159
|
+
if (error instanceof TimeoutError) {
|
|
160
|
+
return { ok: false, code: "timeout", message: "Expansion timed out." };
|
|
161
|
+
}
|
|
162
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
163
|
+
console.error("[prompt-expand] generate failed:", message);
|
|
164
|
+
return { ok: false, code: "failed", message: `Expansion failed: ${message}` };
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
return () => registration.dispose();
|
|
169
|
+
},
|
|
170
|
+
});
|
package/dist/model.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export function composerToRef(model) {
|
|
2
|
+
return {
|
|
3
|
+
providerID: model.providerID,
|
|
4
|
+
id: model.modelID,
|
|
5
|
+
...(model.variant ? { variant: model.variant } : {}),
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function modelToRef(model) {
|
|
9
|
+
return {
|
|
10
|
+
providerID: model.providerID,
|
|
11
|
+
id: model.id,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function modelLabel(model) {
|
|
15
|
+
return `${model.providerID}/${model.id}`;
|
|
16
|
+
}
|
|
17
|
+
export function parseExplicitModel(value) {
|
|
18
|
+
const index = value.indexOf("/");
|
|
19
|
+
if (index <= 0 || index === value.length - 1)
|
|
20
|
+
return null;
|
|
21
|
+
return { providerID: value.slice(0, index), id: value.slice(index + 1) };
|
|
22
|
+
}
|
|
23
|
+
export function findModel(models, ref) {
|
|
24
|
+
return models.find((model) => model.providerID === ref.providerID && (model.id === ref.id || model.modelID === ref.id));
|
|
25
|
+
}
|
|
26
|
+
function baseCost(model) {
|
|
27
|
+
if (!model.cost || model.cost.length === 0)
|
|
28
|
+
return undefined;
|
|
29
|
+
return model.cost.find((tier) => !tier.tier) ?? model.cost[0];
|
|
30
|
+
}
|
|
31
|
+
export function isCheapChat(model) {
|
|
32
|
+
if (model.enabled === false)
|
|
33
|
+
return false;
|
|
34
|
+
if (model.status === "deprecated")
|
|
35
|
+
return false;
|
|
36
|
+
if (model.type && model.type !== "chat")
|
|
37
|
+
return false;
|
|
38
|
+
const cost = baseCost(model);
|
|
39
|
+
if (!cost)
|
|
40
|
+
return true;
|
|
41
|
+
return cost.input < 1 && cost.output < 3;
|
|
42
|
+
}
|
|
43
|
+
function cheapestChat(models) {
|
|
44
|
+
return models
|
|
45
|
+
.filter((model) => isCheapChat(model))
|
|
46
|
+
.slice()
|
|
47
|
+
.sort((left, right) => {
|
|
48
|
+
const leftCost = baseCost(left);
|
|
49
|
+
const rightCost = baseCost(right);
|
|
50
|
+
const leftScore = leftCost ? leftCost.input + leftCost.output : 0;
|
|
51
|
+
const rightScore = rightCost ? rightCost.input + rightCost.output : 0;
|
|
52
|
+
return leftScore - rightScore;
|
|
53
|
+
})[0];
|
|
54
|
+
}
|
|
55
|
+
function anyChat(models) {
|
|
56
|
+
return models.find((model) => model.enabled !== false &&
|
|
57
|
+
model.status !== "deprecated" &&
|
|
58
|
+
(!model.type || model.type === "chat"));
|
|
59
|
+
}
|
|
60
|
+
export function pickModel(input) {
|
|
61
|
+
const preference = input.preference.trim();
|
|
62
|
+
if (preference && preference !== "auto" && preference !== "composer") {
|
|
63
|
+
const explicit = parseExplicitModel(preference);
|
|
64
|
+
if (explicit) {
|
|
65
|
+
const found = findModel(input.models, explicit);
|
|
66
|
+
return { model: found ? modelToRef(found) : explicit, source: "explicit" };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const composerRef = input.composerModel ? composerToRef(input.composerModel) : undefined;
|
|
70
|
+
if (preference === "composer") {
|
|
71
|
+
if (composerRef)
|
|
72
|
+
return { model: composerRef, source: "composer" };
|
|
73
|
+
if (input.sessionModel)
|
|
74
|
+
return { model: input.sessionModel, source: "session" };
|
|
75
|
+
if (input.defaultModel)
|
|
76
|
+
return { model: modelToRef(input.defaultModel), source: "default" };
|
|
77
|
+
const fallback = anyChat(input.models);
|
|
78
|
+
return fallback ? { model: modelToRef(fallback), source: "cheap" } : null;
|
|
79
|
+
}
|
|
80
|
+
// auto: composer model first, but avoid expensive models.
|
|
81
|
+
if (composerRef) {
|
|
82
|
+
const info = findModel(input.models, composerRef);
|
|
83
|
+
if (!info || isCheapChat(info)) {
|
|
84
|
+
return { model: composerRef, source: "composer" };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (input.sessionModel) {
|
|
88
|
+
const info = findModel(input.models, input.sessionModel);
|
|
89
|
+
if (!info || isCheapChat(info)) {
|
|
90
|
+
return { model: input.sessionModel, source: "session" };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// With no composer/session model (Home or a draft), the server default is
|
|
94
|
+
// a better guess than the cheapest model: free models often have hard usage
|
|
95
|
+
// limits, which would make the fallback button fail immediately.
|
|
96
|
+
if (input.defaultModel && input.defaultModel.enabled !== false && input.defaultModel.status !== "deprecated") {
|
|
97
|
+
return { model: modelToRef(input.defaultModel), source: "default" };
|
|
98
|
+
}
|
|
99
|
+
const cheap = cheapestChat(input.models);
|
|
100
|
+
if (cheap)
|
|
101
|
+
return { model: modelToRef(cheap), source: "cheap" };
|
|
102
|
+
const fallback = anyChat(input.models);
|
|
103
|
+
return fallback ? { model: modelToRef(fallback), source: "cheap" } : null;
|
|
104
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const STRUCTURE_MARKERS = [/目标/, /范围/, /要求/, /验收/];
|
|
2
|
+
const CONFIRMATION = /^(继续|好的|收到|谢谢|可以|ok|okay|yes|no|go|continue|thanks?)[。.!!]?$/i;
|
|
3
|
+
const MENTION = /(^|\s)@[\w./\\-]{2,}/;
|
|
4
|
+
export function detectMention(text) {
|
|
5
|
+
return MENTION.test(text);
|
|
6
|
+
}
|
|
7
|
+
export function countStructureMarkers(text) {
|
|
8
|
+
return STRUCTURE_MARKERS.filter((pattern) => pattern.test(text)).length;
|
|
9
|
+
}
|
|
10
|
+
export function hasStructure(text) {
|
|
11
|
+
return countStructureMarkers(text) >= 3;
|
|
12
|
+
}
|
|
13
|
+
export function shouldSkip(text, config) {
|
|
14
|
+
const trimmed = text.trim();
|
|
15
|
+
const structured = hasStructure(trimmed);
|
|
16
|
+
if (!trimmed)
|
|
17
|
+
return { skip: true, reason: "empty", notify: false, structured };
|
|
18
|
+
if (/^\s*[\/!]/.test(trimmed)) {
|
|
19
|
+
return { skip: true, reason: "command", notify: false, structured };
|
|
20
|
+
}
|
|
21
|
+
if (CONFIRMATION.test(trimmed)) {
|
|
22
|
+
return { skip: true, reason: "confirmation", notify: false, structured };
|
|
23
|
+
}
|
|
24
|
+
if (countChars(trimmed) < config.minChars) {
|
|
25
|
+
return { skip: true, reason: "too_short", notify: true, structured };
|
|
26
|
+
}
|
|
27
|
+
if (countChars(trimmed) > config.maxPromptChars) {
|
|
28
|
+
return { skip: true, reason: "too_long", notify: false, structured };
|
|
29
|
+
}
|
|
30
|
+
if (config.skipPatterns.some((pattern) => pattern.test(trimmed))) {
|
|
31
|
+
return { skip: true, reason: "pattern", notify: false, structured };
|
|
32
|
+
}
|
|
33
|
+
if (config.skipOnMentions && detectMention(trimmed)) {
|
|
34
|
+
return { skip: true, reason: "mention", notify: true, structured };
|
|
35
|
+
}
|
|
36
|
+
return { skip: false, notify: false, structured };
|
|
37
|
+
}
|
|
38
|
+
export function countChars(text) {
|
|
39
|
+
return Array.from(text).length;
|
|
40
|
+
}
|
|
41
|
+
function stripCodeFences(text) {
|
|
42
|
+
if (!text.includes("```"))
|
|
43
|
+
return text;
|
|
44
|
+
return text
|
|
45
|
+
.replace(/^\s*```[^\n]*\n?/i, "")
|
|
46
|
+
.replace(/\n?```\s*$/i, "")
|
|
47
|
+
.replace(/^\s*```[^\n]*$/gm, "")
|
|
48
|
+
.trim();
|
|
49
|
+
}
|
|
50
|
+
function stripLeadingLabel(text) {
|
|
51
|
+
return text.replace(/^(优化后的提示词|增强后的提示词|改写后的提示词|增强结果|优化结果|expanded prompt|optimized prompt)\s*[::]\s*/i, "");
|
|
52
|
+
}
|
|
53
|
+
function stripWrappingQuotes(text) {
|
|
54
|
+
const pairs = [
|
|
55
|
+
['"', '"'],
|
|
56
|
+
["'", "'"],
|
|
57
|
+
["“", "”"],
|
|
58
|
+
["‘", "’"],
|
|
59
|
+
["「", "」"],
|
|
60
|
+
["『", "』"],
|
|
61
|
+
];
|
|
62
|
+
for (const [open, close] of pairs) {
|
|
63
|
+
if (text.startsWith(open) && text.endsWith(close) && text.length >= open.length + close.length) {
|
|
64
|
+
return text.slice(open.length, -close.length).trim();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return text;
|
|
68
|
+
}
|
|
69
|
+
export function cleanOutput(text) {
|
|
70
|
+
let result = text.trim();
|
|
71
|
+
result = stripCodeFences(result);
|
|
72
|
+
result = stripLeadingLabel(result);
|
|
73
|
+
result = stripWrappingQuotes(result);
|
|
74
|
+
return result.trim();
|
|
75
|
+
}
|
|
76
|
+
export function validateOutput(output, original, config) {
|
|
77
|
+
const cleaned = output.trim();
|
|
78
|
+
if (!cleaned)
|
|
79
|
+
return { ok: false, reason: "empty" };
|
|
80
|
+
if (cleaned === original.trim())
|
|
81
|
+
return { ok: false, reason: "unchanged" };
|
|
82
|
+
if (countChars(cleaned) > countChars(original) * config.maxGrowth && countChars(cleaned) > 2000) {
|
|
83
|
+
return { ok: false, reason: "too_large" };
|
|
84
|
+
}
|
|
85
|
+
return { ok: true };
|
|
86
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export const RULE_FILES = [
|
|
4
|
+
"AGENTS.md",
|
|
5
|
+
"CLAUDE.md",
|
|
6
|
+
"GEMINI.md",
|
|
7
|
+
".cursorrules",
|
|
8
|
+
".windsurfrules",
|
|
9
|
+
"CONVENTIONS.md",
|
|
10
|
+
".github/copilot-instructions.md",
|
|
11
|
+
];
|
|
12
|
+
const SENSITIVE = /(^|[\\/])(\.env|.*\.pem|.*\.key|credentials.*)$/i;
|
|
13
|
+
const cache = new Map();
|
|
14
|
+
const CACHE_MS = 5 * 60 * 1000;
|
|
15
|
+
export function clearProjectContextCache() {
|
|
16
|
+
cache.clear();
|
|
17
|
+
}
|
|
18
|
+
function truncate(value, limit) {
|
|
19
|
+
if (limit <= 0)
|
|
20
|
+
return "";
|
|
21
|
+
return Array.from(value).slice(0, limit).join("");
|
|
22
|
+
}
|
|
23
|
+
function isSensitive(path) {
|
|
24
|
+
return SENSITIVE.test(path);
|
|
25
|
+
}
|
|
26
|
+
function packageSummary(packageJson) {
|
|
27
|
+
if (!packageJson || typeof packageJson !== "object")
|
|
28
|
+
return "";
|
|
29
|
+
const data = packageJson;
|
|
30
|
+
const lines = [];
|
|
31
|
+
if (typeof data.name === "string")
|
|
32
|
+
lines.push(`name: ${data.name}`);
|
|
33
|
+
if (typeof data.description === "string")
|
|
34
|
+
lines.push(`description: ${data.description}`);
|
|
35
|
+
const dependencies = data.dependencies;
|
|
36
|
+
if (dependencies && typeof dependencies === "object" && !Array.isArray(dependencies)) {
|
|
37
|
+
const names = Object.keys(dependencies);
|
|
38
|
+
if (names.length > 0)
|
|
39
|
+
lines.push(`dependencies: ${names.join(", ")}`);
|
|
40
|
+
}
|
|
41
|
+
return lines.join("\n");
|
|
42
|
+
}
|
|
43
|
+
async function readPackageSummary(directory) {
|
|
44
|
+
const path = join(directory, "package.json");
|
|
45
|
+
if (isSensitive(path))
|
|
46
|
+
return "";
|
|
47
|
+
try {
|
|
48
|
+
const raw = await readFile(path, "utf8");
|
|
49
|
+
return packageSummary(JSON.parse(raw));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function readRules(directory) {
|
|
56
|
+
const chunks = [];
|
|
57
|
+
for (const file of RULE_FILES) {
|
|
58
|
+
const path = join(directory, file);
|
|
59
|
+
if (isSensitive(path))
|
|
60
|
+
continue;
|
|
61
|
+
try {
|
|
62
|
+
const content = await readFile(path, "utf8");
|
|
63
|
+
const trimmed = content.trim();
|
|
64
|
+
if (trimmed)
|
|
65
|
+
chunks.push(`# ${file}\n${trimmed}`);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Missing rule file is expected.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return chunks.join("\n\n");
|
|
72
|
+
}
|
|
73
|
+
export function resolveProjectDirectory(input) {
|
|
74
|
+
return (input.sessionDirectory?.trim() ||
|
|
75
|
+
input.requestDirectory?.trim() ||
|
|
76
|
+
input.pluginDirectory?.trim() ||
|
|
77
|
+
"");
|
|
78
|
+
}
|
|
79
|
+
export function formatProjectContext(parts, budget) {
|
|
80
|
+
if (budget <= 0)
|
|
81
|
+
return "";
|
|
82
|
+
const sections = [];
|
|
83
|
+
if (parts.packageSummary?.trim()) {
|
|
84
|
+
sections.push(`- 技术栈:\n${truncate(parts.packageSummary.trim(), 400)}`);
|
|
85
|
+
}
|
|
86
|
+
if (parts.rules?.trim()) {
|
|
87
|
+
sections.push(`- 规则摘要:\n${truncate(parts.rules.trim(), 1200)}`);
|
|
88
|
+
}
|
|
89
|
+
const gitLines = [];
|
|
90
|
+
if (parts.branch)
|
|
91
|
+
gitLines.push(`- 当前分支:${parts.branch}`);
|
|
92
|
+
if (parts.files && parts.files.length > 0) {
|
|
93
|
+
gitLines.push(`- 改动文件:${parts.files.slice(0, 12).join(", ")}`);
|
|
94
|
+
}
|
|
95
|
+
if (gitLines.length > 0) {
|
|
96
|
+
sections.push(truncate(gitLines.join("\n"), 300));
|
|
97
|
+
}
|
|
98
|
+
return truncate(sections.join("\n"), budget).trim();
|
|
99
|
+
}
|
|
100
|
+
export async function readProjectContext(directory, budget, vcs) {
|
|
101
|
+
if (budget <= 0)
|
|
102
|
+
return "";
|
|
103
|
+
const files = (vcs?.files ?? []).filter((file) => !file.includes("node_modules/")).slice(0, 12);
|
|
104
|
+
const key = [directory, budget, vcs?.branch ?? "", files.join(",")].join("\0");
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
const cached = cache.get(key);
|
|
107
|
+
if (cached && cached.expires > now)
|
|
108
|
+
return cached.value;
|
|
109
|
+
const [packageSummary, rules] = await Promise.all([
|
|
110
|
+
readPackageSummary(directory),
|
|
111
|
+
readRules(directory),
|
|
112
|
+
]);
|
|
113
|
+
const value = formatProjectContext({
|
|
114
|
+
packageSummary,
|
|
115
|
+
rules,
|
|
116
|
+
branch: vcs?.branch,
|
|
117
|
+
files,
|
|
118
|
+
}, budget);
|
|
119
|
+
cache.set(key, { value, expires: now + CACHE_MS });
|
|
120
|
+
return value;
|
|
121
|
+
}
|