aitable-workflow-cli 0.1.23 → 0.1.24
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/README.md +1 -6
- package/dist/{chunk-IGB6WDLR.js → chunk-UNPV6DTX.js} +1 -1
- package/dist/cli.js +47 -47
- package/dist/config-set-FBOQBRSJ.js +13 -0
- package/dist/{config-ui-server-CORDAOYI.js → config-ui-server-IUSACAOT.js} +1 -1
- package/package.json +3 -3
- package/dist/config-set-EGLLIMVJ.js +0 -13
- package/templates/support-qa/.env.example +0 -16
- package/templates/support-qa/aitable-workflow.config.yml +0 -46
- package/templates/support-qa/coding-agents.config.json +0 -18
- package/templates/support-qa/lib/__tests__/declarative-matcher.test.ts +0 -240
- package/templates/support-qa/lib/__tests__/dingtalk-group-membership.test.ts +0 -144
- package/templates/support-qa/lib/__tests__/memory-events.test.ts +0 -90
- package/templates/support-qa/lib/__tests__/sandbox-agent.test.ts +0 -382
- package/templates/support-qa/lib/__tests__/thread-adapter.test.ts +0 -242
- package/templates/support-qa/lib/__tests__/thread-observe-step.test.ts +0 -391
- package/templates/support-qa/lib/__tests__/thread-observer-handler.test.ts +0 -274
- package/templates/support-qa/lib/action-dispatch.ts +0 -262
- package/templates/support-qa/lib/contract.ts +0 -253
- package/templates/support-qa/lib/declarative-matcher.ts +0 -398
- package/templates/support-qa/lib/dingtalk-group-membership.ts +0 -158
- package/templates/support-qa/lib/guards.ts +0 -18
- package/templates/support-qa/lib/memory-events.ts +0 -113
- package/templates/support-qa/lib/owner-resolver.ts +0 -94
- package/templates/support-qa/lib/preflight.ts +0 -294
- package/templates/support-qa/lib/profile.ts +0 -51
- package/templates/support-qa/lib/sandbox-agent.ts +0 -653
- package/templates/support-qa/lib/thread-adapter.ts +0 -281
- package/templates/support-qa/lib/thread-digest.ts +0 -144
- package/templates/support-qa/npmrc +0 -1
- package/templates/support-qa/package.json +0 -27
- package/templates/support-qa/profiles/aitable/profile/escalation/contacts.json +0 -37
- package/templates/support-qa/profiles/aitable/profile/escalation/owner-map.json +0 -18
- package/templates/support-qa/profiles/aitable/profile/fastpath/known-answers.yml +0 -561
- package/templates/support-qa/profiles/aitable/profile/profile.yml +0 -219
- package/templates/support-qa/profiles/aitable/profile/prompts/kb-editor.md +0 -3
- package/templates/support-qa/profiles/aitable/profile/prompts/persona.md +0 -49
- package/templates/support-qa/profiles/aitable/profile/prompts/troubleshooter.md +0 -5
- package/templates/support-qa/profiles/aitable/profile/troubleshoot/classifiers.yml +0 -36
- package/templates/support-qa/profiles/aitable/skills/troubleshooting/SKILL.md +0 -58
- package/templates/support-qa/profiles/aitable/skills/troubleshooting/aitable-import-troubleshooter/SKILL.md +0 -485
- package/templates/support-qa/profiles/aitable/skills/troubleshooting/aitable-openapi-troubleshooter/SKILL.md +0 -183
- package/templates/support-qa/profiles/aitable/skills/troubleshooting/import-openapi-e2e-troubleshooter/SKILL.md +0 -459
- package/templates/support-qa/profiles/aitable/skills/troubleshooting/notable-datasource-troubleshooter/SKILL.md +0 -1286
- package/templates/support-qa/profiles/aitable/skills/troubleshooting/notable-field-troubleshooter/SKILL.md +0 -673
- package/templates/support-qa/profiles/aitable/skills/troubleshooting/spreadsheet-datasync-troubleshooter/SKILL.md +0 -322
- package/templates/support-qa/profiles/default/profile/fastpath/known-answers.yml +0 -32
- package/templates/support-qa/profiles/default/profile/profile.yml +0 -217
- package/templates/support-qa/profiles/default/profile/troubleshoot/classifiers.yml +0 -37
- package/templates/support-qa/profiles/default/wiki/support//345/270/270/350/247/201/351/227/256/351/242/230.md +0 -10
- package/templates/support-qa/tsconfig.json +0 -16
- package/templates/support-qa/vitest.config.ts +0 -9
- package/templates/support-qa/workflows/admin-kb-update/recipes/kb-refine.recipe.ts +0 -137
- package/templates/support-qa/workflows/admin-kb-update/recipes/kb_sync_handler.recipe.ts +0 -119
- package/templates/support-qa/workflows/admin-kb-update/workflow.yml +0 -96
- package/templates/support-qa/workflows/config-sync/recipes/config_sync.recipe.ts +0 -668
- package/templates/support-qa/workflows/config-sync/recipes/config_sync_handler.recipe.ts +0 -16
- package/templates/support-qa/workflows/config-sync/workflow.yml +0 -62
- package/templates/support-qa/workflows/daily-review/recipes/daily_review.recipe.ts +0 -602
- package/templates/support-qa/workflows/daily-review/recipes/daily_review_handler.recipe.ts +0 -26
- package/templates/support-qa/workflows/daily-review/workflow.yml +0 -65
- package/templates/support-qa/workflows/group-qa/recipes/answer.recipe.ts +0 -231
- package/templates/support-qa/workflows/group-qa/recipes/conversation_handler.recipe.ts +0 -394
- package/templates/support-qa/workflows/group-qa/recipes/fastpath.recipe.ts +0 -72
- package/templates/support-qa/workflows/group-qa/recipes/on-action.recipe.ts +0 -225
- package/templates/support-qa/workflows/group-qa/recipes/reply-to-group.recipe.ts +0 -202
- package/templates/support-qa/workflows/group-qa/recipes/troubleshoot.recipe.ts +0 -273
- package/templates/support-qa/workflows/group-qa/workflow.yml +0 -268
- package/templates/support-qa/workflows/thread-observer/README.md +0 -96
- package/templates/support-qa/workflows/thread-observer/recipes/thread_observe.recipe.ts +0 -415
- package/templates/support-qa/workflows/thread-observer/recipes/thread_observer_handler.recipe.ts +0 -213
- package/templates/support-qa/workflows/thread-observer/workflow.yml +0 -185
- package/templates/support-qa/workspaces/daily-review/AGENTS.md +0 -233
- package/templates/support-qa/workspaces/kb-editor/AGENTS.md +0 -32
- package/templates/support-qa/workspaces/support/AGENTS.md +0 -156
- package/templates/support-qa/workspaces/thread-observer/AGENTS.md +0 -156
- package/templates/support-qa/workspaces/troubleshooter/AGENTS.md +0 -71
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import{a}from"./chunk-EI7MMDWY.js";import O from"path";import{applyConfigSave as P,collectAllItems as R,resolveConfigPath as T,runCheck as b}from"aitable-workflow-core";function x(e){let n=[];for(let t of e.modules)if(t.workflows)for(let o of t.workflows)for(let r of o.items)n.push({item:r,workflowId:o.id});else for(let o of t.items)n.push({item:o});return n}a(x,"collectScopedItems");function j(e){let n=[];for(let t of e.modules)if(t.workflows)for(let o of t.workflows)for(let r of o.surface??[])n.push({item:{key:r.key,status:"ok",writeTarget:r.writeTarget,sensitive:r.owner==="env"},workflowId:o.id});return n}a(j,"collectScopedSurface");var c=class extends Error{static{a(this,"ConfigSetError")}constructor(n){super(n),this.name="ConfigSetError"}};function J(e){switch(e.type){case"yaml":return`${e.file} \u2192 ${e.yamlPath.join(".")}`;case"yamlAll":return e.targets.map(n=>`${n.file} \u2192 ${n.yamlPath.join(".")}`).join(" | ");case"json":return`${e.file} \u2192 ${e.jsonPath.join(".")}`;case"env":return`${e.file} \u2192 ${e.envKey}`;case"textFile":return e.file;default:return JSON.stringify(e)}}a(J,"describeWriteTarget");function N(e){let n=e.indexOf("=");if(n<=0)throw new c(`--set \u9700\u8981 key=value \u5F62\u5F0F\uFF0C\u6536\u5230 ${JSON.stringify(e)}`);return{key:e.slice(0,n).trim(),value:e.slice(n+1)}}a(N,"parseSetPair");function A(e,n){let t=[...new Set(n.map(({item:g})=>g.key))],o=e.toLowerCase(),r=t.filter(g=>g.toLowerCase().includes(o)),i=r.length>0?r:t,f=i.slice(0,30),u=i.length>f.length?`
|
|
2
|
+
\u2026\u53E6\u6709 ${i.length-f.length} \u9879\uFF0C\u8DD1 \`config --json\` \u770B\u5168\u91CF`:"";return new c(`\u672A\u77E5\u914D\u7F6E\u9879 ${JSON.stringify(e)}\u3002${r.length>0?"\u76F8\u8FD1\u7684":"\u53EF\u5199\u7684"} key\uFF1A
|
|
3
|
+
${f.join(`
|
|
4
|
+
`)}${u}`)}a(A,"unknownKeyError");function E(e,n,t){let o=e.filter(({item:i})=>i.key===t);if(o.length>1){let i=o.map(({workflowId:f})=>f??"(\u9879\u76EE\u7EA7)");throw new c(`\u914D\u7F6E\u9879 ${JSON.stringify(t)} \u5728\u591A\u5904\u90FD\u5B58\u5728\uFF08${i.join(", ")}\uFF09\uFF0C\u65E0\u6CD5\u5224\u65AD\u8981\u6539\u54EA\u4E00\u4E2A\u3002\u7528 --workflow <id> \u6307\u5B9A\u3002`)}if(o.length===1){let i=o[0].item;if(!i.writeTarget)throw new c(`\u914D\u7F6E\u9879 ${JSON.stringify(t)} \u6CA1\u6709\u5199\u56DE\u76EE\u6807\uFF08\u53EA\u8BFB\u9879\uFF09\uFF0C\u4E0D\u80FD\u7528 config set \u4FEE\u6539`);return i}let r=n.filter(({item:i})=>i.key===t);if(r.length>1){let i=r.map(({workflowId:f})=>f??"(\u9879\u76EE\u7EA7)");throw new c(`\u914D\u7F6E\u9879 ${JSON.stringify(t)} \u5728\u591A\u5904\u90FD\u5B58\u5728\uFF08${i.join(", ")}\uFF09\uFF0C\u65E0\u6CD5\u5224\u65AD\u8981\u6539\u54EA\u4E00\u4E2A\u3002\u7528 --workflow <id> \u6307\u5B9A\u3002`)}if(r.length===1){let i=r[0].item;if(!i.writeTarget)throw new c(`\u914D\u7F6E\u9879 ${JSON.stringify(t)} \u6CA1\u6709\u5199\u56DE\u76EE\u6807\uFF08\u53EA\u8BFB\u9879\uFF09\uFF0C\u4E0D\u80FD\u7528 config set \u4FEE\u6539`);return i}throw A(t,n)}a(E,"resolveItem");async function B(){let e=[];for await(let n of process.stdin)e.push(Buffer.isBuffer(n)?n:Buffer.from(String(n)));return Buffer.concat(e).toString("utf8")}a(B,"readAllStdin");function W(e,n){if(e.length===0)return[];let t=n.replace(/\r\n/g,`
|
|
5
|
+
`).replace(/\n$/,"").split(`
|
|
6
|
+
`);if(t.length<e.length)throw new c(`--set-stdin \u58F0\u660E\u4E86 ${e.length} \u4E2A key\uFF0C\u4F46 stdin \u53EA\u6709 ${t.length} \u884C\u3002\u6BCF\u4E2A key \u4E00\u884C\uFF0C\u987A\u5E8F\u5BF9\u4F4D\u3002`);if(t.length>e.length)throw new c(`--set-stdin \u58F0\u660E\u4E86 ${e.length} \u4E2A key\uFF0C\u4F46 stdin \u6709 ${t.length} \u884C\u3002\u591A\u4F59\u7684\u884C\u4E0D\u4F1A\u88AB\u5199\u5165\uFF0C\u8BF7\u6838\u5BF9\u987A\u5E8F\u3002`);return e.map((o,r)=>({key:o,value:t[r]}))}a(W,"pairStdinValues");async function L(e){let n=e.set??[],t=e.setStdin??[];if(n.length===0&&t.length===0)throw new c("\u81F3\u5C11\u8981\u7ED9\u4E00\u4E2A --set key=value \u6216 --set-stdin key");let o=await T(e.configPath),r=O.dirname(o),i=await b({configPath:o,...e.workflowId?{workflowId:e.workflowId}:{}}),f=x(i),u=j(i),g=n.map(N),d=t.map(s=>s.trim()),S=d.length>0?W(d,await(e.readStdin??B)()):[],m=new Set,w=[];for(let[s,C]of[["argv",g],["stdin",S]])for(let{key:l,value:I}of C){if(m.has(l))throw new c(`\u914D\u7F6E\u9879 ${JSON.stringify(l)} \u88AB\u6307\u5B9A\u4E86\u4E24\u6B21\uFF0C\u53BB\u6389\u91CD\u590D\u7684\u90A3\u4E2A`);let y=E(f,u,l);m.add(l);let k=y.sensitive===!0;if(k&&s==="argv")throw new c(`${l} \u662F\u654F\u611F\u9879\uFF0C\u4E0D\u80FD\u7528 --set \u4F20\uFF08\u503C\u4F1A\u8FDB argv\uFF0C\u4ECE\u800C\u843D\u8FDB\u8FDB\u7A0B\u5217\u8868\u4E0E shell \u5386\u53F2\uFF09\u3002\u6539\u7528 --set-stdin ${l} \u4ECE stdin \u5582\u503C\u3002`);w.push({key:l,writeTarget:y.writeTarget,sensitive:k,via:s,value:I})}let h=w.map(s=>({key:s.key,target:J(s.writeTarget),via:s.via,sensitive:s.sensitive}));if(e.dryRun)return{ok:!0,dryRun:!0,written:0,plan:h,remaining:[],configOk:i.ok};let{written:$,recheck:p}=await P({updates:w.map(s=>({writeTarget:s.writeTarget,value:s.value}))},r,o),v=R(p).filter(s=>s.status==="error").map(s=>({key:s.key,...s.message?{message:s.message}:{}}));return{ok:!0,dryRun:!1,written:$,plan:h,remaining:v,configOk:p.ok}}a(L,"runConfigSet");function V(e,n){if(n){process.stdout.write(JSON.stringify(e,null,2)+`
|
|
7
|
+
`);return}e.dryRun?console.log(`
|
|
8
|
+
\u{1F50E} dry-run\uFF1A\u4EE5\u4E0B\u914D\u7F6E\u9879\u5C06\u88AB\u5199\u5165\uFF08\u672A\u5B9E\u9645\u5199\u76D8\uFF09
|
|
9
|
+
`):console.log(`
|
|
10
|
+
\u2713 \u5DF2\u5199\u5165 ${e.written} \u5904
|
|
11
|
+
`);for(let t of e.plan){let o=t.sensitive?" [\u654F\u611F\u503C\uFF0C\u4E0D\u56DE\u663E]":"";console.log(` ${t.key} \u2192 ${t.target}${o}`)}if(!e.dryRun){if(e.configOk){console.log(`
|
|
12
|
+
\u914D\u7F6E\u6821\u9A8C\u901A\u8FC7\u3002`);return}console.log(`
|
|
13
|
+
\u26A0 \u914D\u7F6E\u4ECD\u6709 ${e.remaining.length} \u9879\u672A\u901A\u8FC7\uFF1A`);for(let t of e.remaining.slice(0,20))console.log(` ${t.key}${t.message?` \u2014\u2014 ${t.message}`:""}`);e.remaining.length>20&&console.log(` \u2026\u53E6\u6709 ${e.remaining.length-20} \u9879`)}}a(V,"printConfigSetResult");export{c as ConfigSetError,J as describeWriteTarget,W as pairStdinValues,N as parseSetPair,V as printConfigSetResult,L as runConfigSet};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{c as a,d as b,e as c,f as d,g as e,h as f,i as g,j as h,k as i,l as j,m as k,n as l,o as m,p as n,q as o,r as p,s as q,t as r,u as s,v as t,w as u}from"./chunk-
|
|
1
|
+
import{c as a,d as b,e as c,f as d,g as e,h as f,i as g,j as h,k as i,l as j,m as k,n as l,o as m,p as n,q as o,r as p,s as q,t as r,u as s,v as t,w as u}from"./chunk-UNPV6DTX.js";import"./chunk-4B6V7VHM.js";import"./chunk-EI7MMDWY.js";export{h as CONSOLE_ACCESS_FILE_REL,d as PromotionHttpError,c as TriggerOnceHttpError,j as clearConsoleAccessFile,a as consoleStartPath,b as createIdleTimer,e as detectEnvironment,l as getRuntimeRunStatus,t as getSetupProgressForTest,p as handleConsolePromotion,m as handleRuntimeRunControl,n as handleTriggerWorkflowOnce,g as hasWebRoot,r as resetPromotionForTest,q as resetRuntimeRunStatusForTest,o as resetTriggerOnceForTest,f as resolveWebRoot,k as runConfigUI,u as serveStatic,s as setSetupProgressForTest,i as writeConsoleAccessFile};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aitable-workflow-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"description": "aitable-workflow 命令行工具(cli):init/design/setup/run/serve 等命令;含 relay 本地反连客户端与 daemon 进程管理。bin: aitable-workflow。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"access": "public"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"aitable-workflow-core": "0.1.
|
|
23
|
-
"aitable-workflow-server": "0.1.
|
|
22
|
+
"aitable-workflow-core": "0.1.24",
|
|
23
|
+
"aitable-workflow-server": "0.1.24",
|
|
24
24
|
"@inquirer/prompts": "^8.5.2",
|
|
25
25
|
"commander": "^14.0.2",
|
|
26
26
|
"dotenv": "^17.4.2",
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import{a as s}from"./chunk-EI7MMDWY.js";import I from"path";import{applyConfigSave as P,collectAllItems as O,resolveConfigPath as R,runCheck as b}from"aitable-workflow-core";function x(e){let n=[];for(let t of e.modules)if(t.workflows)for(let r of t.workflows)for(let o of r.items)n.push({item:o,workflowId:r.id});else for(let r of t.items)n.push({item:r});return n}s(x,"collectScopedItems");var a=class extends Error{static{s(this,"ConfigSetError")}constructor(n){super(n),this.name="ConfigSetError"}};function T(e){switch(e.type){case"yaml":return`${e.file} \u2192 ${e.yamlPath.join(".")}`;case"yamlAll":return e.targets.map(n=>`${n.file} \u2192 ${n.yamlPath.join(".")}`).join(" | ");case"json":return`${e.file} \u2192 ${e.jsonPath.join(".")}`;case"env":return`${e.file} \u2192 ${e.envKey}`;case"textFile":return e.file;default:return JSON.stringify(e)}}s(T,"describeWriteTarget");function j(e){let n=e.indexOf("=");if(n<=0)throw new a(`--set \u9700\u8981 key=value \u5F62\u5F0F\uFF0C\u6536\u5230 ${JSON.stringify(e)}`);return{key:e.slice(0,n).trim(),value:e.slice(n+1)}}s(j,"parseSetPair");function A(e,n){let t=[...new Set(n.filter(({item:g})=>g.writeTarget).map(({item:g})=>g.key))],r=e.toLowerCase(),o=t.filter(g=>g.toLowerCase().includes(r)),l=o.length>0?o:t,f=l.slice(0,30),u=l.length>f.length?`
|
|
2
|
-
\u2026\u53E6\u6709 ${l.length-f.length} \u9879\uFF0C\u8DD1 \`config --json\` \u770B\u5168\u91CF`:"";return new a(`\u672A\u77E5\u914D\u7F6E\u9879 ${JSON.stringify(e)}\u3002${o.length>0?"\u76F8\u8FD1\u7684":"\u53EF\u5199\u7684"} key\uFF1A
|
|
3
|
-
${f.join(`
|
|
4
|
-
`)}${u}`)}s(A,"unknownKeyError");function J(e,n){let t=e.filter(({item:o})=>o.key===n);if(t.length===0)throw A(n,e);if(t.length>1){let o=t.map(({workflowId:l})=>l??"(\u9879\u76EE\u7EA7)");throw new a(`\u914D\u7F6E\u9879 ${JSON.stringify(n)} \u5728\u591A\u5904\u90FD\u5B58\u5728\uFF08${o.join(", ")}\uFF09\uFF0C\u65E0\u6CD5\u5224\u65AD\u8981\u6539\u54EA\u4E00\u4E2A\u3002\u7528 --workflow <id> \u6307\u5B9A\u3002`)}let r=t[0].item;if(!r.writeTarget)throw new a(`\u914D\u7F6E\u9879 ${JSON.stringify(n)} \u6CA1\u6709\u5199\u56DE\u76EE\u6807\uFF08\u53EA\u8BFB\u9879\uFF09\uFF0C\u4E0D\u80FD\u7528 config set \u4FEE\u6539`);return r}s(J,"resolveItem");async function N(){let e=[];for await(let n of process.stdin)e.push(Buffer.isBuffer(n)?n:Buffer.from(String(n)));return Buffer.concat(e).toString("utf8")}s(N,"readAllStdin");function E(e,n){if(e.length===0)return[];let t=n.replace(/\r\n/g,`
|
|
5
|
-
`).replace(/\n$/,"").split(`
|
|
6
|
-
`);if(t.length<e.length)throw new a(`--set-stdin \u58F0\u660E\u4E86 ${e.length} \u4E2A key\uFF0C\u4F46 stdin \u53EA\u6709 ${t.length} \u884C\u3002\u6BCF\u4E2A key \u4E00\u884C\uFF0C\u987A\u5E8F\u5BF9\u4F4D\u3002`);if(t.length>e.length)throw new a(`--set-stdin \u58F0\u660E\u4E86 ${e.length} \u4E2A key\uFF0C\u4F46 stdin \u6709 ${t.length} \u884C\u3002\u591A\u4F59\u7684\u884C\u4E0D\u4F1A\u88AB\u5199\u5165\uFF0C\u8BF7\u6838\u5BF9\u987A\u5E8F\u3002`);return e.map((r,o)=>({key:r,value:t[o]}))}s(E,"pairStdinValues");async function K(e){let n=e.set??[],t=e.setStdin??[];if(n.length===0&&t.length===0)throw new a("\u81F3\u5C11\u8981\u7ED9\u4E00\u4E2A --set key=value \u6216 --set-stdin key");let r=await R(e.configPath),o=I.dirname(r),l=await b({configPath:r,...e.workflowId?{workflowId:e.workflowId}:{}}),f=x(l),u=n.map(j),g=t.map(i=>i.trim()),k=g.length>0?E(g,await(e.readStdin??N)()):[],w=new Set,d=[];for(let[i,$]of[["argv",u],["stdin",k]])for(let{key:c,value:C}of $){if(w.has(c))throw new a(`\u914D\u7F6E\u9879 ${JSON.stringify(c)} \u88AB\u6307\u5B9A\u4E86\u4E24\u6B21\uFF0C\u53BB\u6389\u91CD\u590D\u7684\u90A3\u4E2A`);let p=J(f,c);w.add(c);let y=p.sensitive===!0;if(y&&i==="argv")throw new a(`${c} \u662F\u654F\u611F\u9879\uFF0C\u4E0D\u80FD\u7528 --set \u4F20\uFF08\u503C\u4F1A\u8FDB argv\uFF0C\u4ECE\u800C\u843D\u8FDB\u8FDB\u7A0B\u5217\u8868\u4E0E shell \u5386\u53F2\uFF09\u3002\u6539\u7528 --set-stdin ${c} \u4ECE stdin \u5582\u503C\u3002`);d.push({key:c,writeTarget:p.writeTarget,sensitive:y,via:i,value:C})}let m=d.map(i=>({key:i.key,target:T(i.writeTarget),via:i.via,sensitive:i.sensitive}));if(e.dryRun)return{ok:!0,dryRun:!0,written:0,plan:m,remaining:[],configOk:l.ok};let{written:S,recheck:h}=await P({updates:d.map(i=>({writeTarget:i.writeTarget,value:i.value}))},o,r),v=O(h).filter(i=>i.status==="error").map(i=>({key:i.key,...i.message?{message:i.message}:{}}));return{ok:!0,dryRun:!1,written:S,plan:m,remaining:v,configOk:h.ok}}s(K,"runConfigSet");function F(e,n){if(n){process.stdout.write(JSON.stringify(e,null,2)+`
|
|
7
|
-
`);return}e.dryRun?console.log(`
|
|
8
|
-
\u{1F50E} dry-run\uFF1A\u4EE5\u4E0B\u914D\u7F6E\u9879\u5C06\u88AB\u5199\u5165\uFF08\u672A\u5B9E\u9645\u5199\u76D8\uFF09
|
|
9
|
-
`):console.log(`
|
|
10
|
-
\u2713 \u5DF2\u5199\u5165 ${e.written} \u5904
|
|
11
|
-
`);for(let t of e.plan){let r=t.sensitive?" [\u654F\u611F\u503C\uFF0C\u4E0D\u56DE\u663E]":"";console.log(` ${t.key} \u2192 ${t.target}${r}`)}if(!e.dryRun){if(e.configOk){console.log(`
|
|
12
|
-
\u914D\u7F6E\u6821\u9A8C\u901A\u8FC7\u3002`);return}console.log(`
|
|
13
|
-
\u26A0 \u914D\u7F6E\u4ECD\u6709 ${e.remaining.length} \u9879\u672A\u901A\u8FC7\uFF1A`);for(let t of e.remaining.slice(0,20))console.log(` ${t.key}${t.message?` \u2014\u2014 ${t.message}`:""}`);e.remaining.length>20&&console.log(` \u2026\u53E6\u6709 ${e.remaining.length-20} \u9879`)}}s(F,"printConfigSetResult");export{a as ConfigSetError,T as describeWriteTarget,E as pairStdinValues,j as parseSetPair,F as printConfigSetResult,K as runConfigSet};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# LLM(知识库编译 + 答疑推理)
|
|
2
|
-
# 沙箱模式下 qodercli 在沙箱内运行,本地不需要 IDEALAB_API_KEY;
|
|
3
|
-
# 如需本地 fallback 调试可保留。
|
|
4
|
-
# IDEALAB_API_KEY=your-idealab-key
|
|
5
|
-
|
|
6
|
-
# Aone Sandbox 凭据(support-qa 沙箱模式必填)
|
|
7
|
-
SANDBOX_API_KEY=your-sandbox-api-key
|
|
8
|
-
QODER_PERSONAL_ACCESS_TOKEN=your-qoder-pat
|
|
9
|
-
|
|
10
|
-
# dws CLI 需在 PATH:消息拉取(群答疑)、todo(知识缺口待办通知)
|
|
11
|
-
# L2 装 config-sync 包后:群配置与管理员名单来自 globalVar(group_configs/admins)
|
|
12
|
-
|
|
13
|
-
# 出站/入站凭据不再配 env:群 Webhook 凭据(webhookUrl/webhookSecret)与企业机器人
|
|
14
|
-
# 凭据(appKey/appSecret)直接填在 AI 表格「群配置表」对应列,config-sync 定时拉取
|
|
15
|
-
# 发布到 globalVar(webhook_creds/robot_creds),加群只改表、无需改 env 重启。
|
|
16
|
-
# 缺凭据的群会在配置同步留痕表的「同步结果」列显式报错。
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# aitable-workflow 配置(support-qa 模板)
|
|
2
|
-
# 由 `aitable-workflow init --template support-qa` 生成
|
|
3
|
-
|
|
4
|
-
# ─── 场景描述(design 命令的核心输入) ─────────────
|
|
5
|
-
scene: |
|
|
6
|
-
场景:通用群知识库答疑助手(support-qa Pattern L0 主环)
|
|
7
|
-
|
|
8
|
-
监听钉钉群消息,基于 profile 指定的知识库自动答疑:
|
|
9
|
-
1. 闸门判定 → 单个 coding agent 依据知识库产出严格 JSON
|
|
10
|
-
{action, reason, reply?, module?};无据/越界/有风险一律静默
|
|
11
|
-
2. 拍平处置动作(reply/bug/feature/kb_gap/no_question/human_handled/owner_mentioned/safety/agent_failure)
|
|
12
|
-
→ on_action 步按 profile 决定留痕(track)与群内是否说话(group:answer/silent);
|
|
13
|
-
bug/feature/kb_gap 确定性解析模块 Owner,解析成功则 Runtime 派生附加 Owner 尾注(与 group 正交),
|
|
14
|
-
同时写渠道无关的通知字段供 AI 表格自动化消费
|
|
15
|
-
3. kb_gap 缺口进「知识收件箱」视图,管理员补充后走 admin-kb-update 轨道入库(L1 包)
|
|
16
|
-
4. 回复通过 messaging toolkit 自动选路发送,发送成功才算 answered,并发布回声指纹防自激
|
|
17
|
-
|
|
18
|
-
业务差异(产品名/知识库/转人工目录/秒回规则/诊断 Skill/补位开关)全部在 profile/profile.yml,
|
|
19
|
-
通用工作流与 recipes 不含任何产品硬编码。
|
|
20
|
-
|
|
21
|
-
# ─── 工作流目录(多工作流)────────────────────────
|
|
22
|
-
workflows:
|
|
23
|
-
dir: "./workflows"
|
|
24
|
-
|
|
25
|
-
# ─── Agent 引擎 ──────────────────────────────────
|
|
26
|
-
agent:
|
|
27
|
-
# engine 留空:首次运行 `aitable-workflow config` 时检测本机 coding agent 并回填
|
|
28
|
-
engine:
|
|
29
|
-
max_concurrent: 3
|
|
30
|
-
timeout_ms: 600000
|
|
31
|
-
max_step_failures: 5
|
|
32
|
-
trace_dir: "./traces"
|
|
33
|
-
|
|
34
|
-
# ─── Recipe 配置 ─────────────────────────────────
|
|
35
|
-
recipe:
|
|
36
|
-
default_timeout_ms: 300000
|
|
37
|
-
|
|
38
|
-
# ─── 轮询配置 ────────────────────────────────────
|
|
39
|
-
polling:
|
|
40
|
-
interval_ms: 5000
|
|
41
|
-
|
|
42
|
-
# ─── SLA 配置 ─────────────────────────────────────
|
|
43
|
-
sla:
|
|
44
|
-
enabled: true
|
|
45
|
-
default_timeout_ms: 86400000
|
|
46
|
-
escalation_notify: []
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 1,
|
|
3
|
-
"agents": {
|
|
4
|
-
"claudecode": {
|
|
5
|
-
"enabled": true,
|
|
6
|
-
"cmd": "claude"
|
|
7
|
-
},
|
|
8
|
-
"codex": {
|
|
9
|
-
"enabled": true,
|
|
10
|
-
"permissionMode": "bypassPermissions"
|
|
11
|
-
},
|
|
12
|
-
"qoder": {
|
|
13
|
-
"enabled": true,
|
|
14
|
-
"permissionMode": "bypassPermissions",
|
|
15
|
-
"model": "Efficient"
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 声明式规则匹配引擎(support-qa/lib/declarative-matcher)
|
|
3
|
-
*
|
|
4
|
-
* 断言逐条沿用迁移前的用例,锁死四条硬不变量:
|
|
5
|
-
* 单向匹配 / 只看用户本轮新话(剥离引用块)/ safety 看全文 / 优雅关闭。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
9
|
-
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
10
|
-
import path from "node:path";
|
|
11
|
-
import os from "node:os";
|
|
12
|
-
import {
|
|
13
|
-
loadReplyRules,
|
|
14
|
-
matchReplyRules,
|
|
15
|
-
stripQuotedBlock,
|
|
16
|
-
loadClassifierRules,
|
|
17
|
-
classifyMessage,
|
|
18
|
-
isCapabilityEnabled,
|
|
19
|
-
isGroupToggleEnabled,
|
|
20
|
-
} from "../declarative-matcher.js";
|
|
21
|
-
|
|
22
|
-
describe("loadReplyRules / matchReplyRules:声明式秒回规则 + 单向匹配 + 安全兜底", () => {
|
|
23
|
-
let dir: string;
|
|
24
|
-
beforeEach(async () => { dir = await mkdtemp(path.join(os.tmpdir(), "sq-fp-lib-")); });
|
|
25
|
-
afterEach(async () => { await rm(dir, { recursive: true, force: true }); });
|
|
26
|
-
|
|
27
|
-
async function writeRules(text: string): Promise<string> {
|
|
28
|
-
await mkdir(path.join(dir, "profile", "fastpath"), { recursive: true });
|
|
29
|
-
await writeFile(path.join(dir, "profile", "fastpath", "known-answers.yml"), text, "utf8");
|
|
30
|
-
return "fastpath/known-answers.yml";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
it("未配置 rules → config=null(fastpath 关闭)", () => {
|
|
34
|
-
expect(loadReplyRules(dir, undefined).config).toBeNull();
|
|
35
|
-
expect(loadReplyRules(dir, "").config).toBeNull();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("文件缺失 → config=null + issue(优雅关闭,不阻断)", () => {
|
|
39
|
-
const r = loadReplyRules(dir, "fastpath/missing.yml");
|
|
40
|
-
expect(r.config).toBeNull();
|
|
41
|
-
expect(r.issues.join("|")).toContain("不存在");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("contains 单向匹配:消息包含规则模式命中;反向(规则含消息)绝不命中(修早期双向 includes bug)", async () => {
|
|
45
|
-
const rel = await writeRules([
|
|
46
|
-
"rules:",
|
|
47
|
-
" - { key: row_limit, contains: 多少行, reply: 单表上限 20 万行。 }",
|
|
48
|
-
].join("\n"));
|
|
49
|
-
const { config } = loadReplyRules(dir, rel);
|
|
50
|
-
// 正向:消息包含 "多少行" → 命中
|
|
51
|
-
expect(matchReplyRules(config, "请问单表支持多少行数据")).toMatchObject({ key: "row_limit", kind: "contains" });
|
|
52
|
-
// 反向:消息 "行" 是规则模式的子串,但消息不包含规则模式 → 绝不命中
|
|
53
|
-
expect(matchReplyRules(config, "行")).toBeNull();
|
|
54
|
-
expect(matchReplyRules(config, "多少")).toBeNull();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("regex / template / 未命中", async () => {
|
|
58
|
-
const rel = await writeRules([
|
|
59
|
-
"rules:",
|
|
60
|
-
" - { key: rx, regex: '(最多|上限).{0,4}行', reply: R1 }",
|
|
61
|
-
" - { key: tpl, template: '完全固定的问法', reply: R2 }",
|
|
62
|
-
].join("\n"));
|
|
63
|
-
const { config } = loadReplyRules(dir, rel);
|
|
64
|
-
expect(matchReplyRules(config, "最多几行")).toMatchObject({ key: "rx", kind: "regex" });
|
|
65
|
-
expect(matchReplyRules(config, " 完全固定的问法 ")).toMatchObject({ key: "tpl", kind: "template" }); // 空白归一
|
|
66
|
-
expect(matchReplyRules(config, "完全固定的问法啊")).toBeNull(); // template 需全等
|
|
67
|
-
expect(matchReplyRules(config, "无关消息")).toBeNull();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("safety 安全兜底优先级最高(先于普通规则)", async () => {
|
|
71
|
-
const rel = await writeRules([
|
|
72
|
-
"safety: { keywords: [涉及敏感内容], reply: 安全话术 }",
|
|
73
|
-
"rules:",
|
|
74
|
-
" - { key: any, contains: 内容, reply: 普通话术 }",
|
|
75
|
-
].join("\n"));
|
|
76
|
-
const { config } = loadReplyRules(dir, rel);
|
|
77
|
-
expect(matchReplyRules(config, "这条涉及敏感内容的消息")).toMatchObject({ key: "safety", kind: "safety", reply: "安全话术" });
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("规则形状非法 → 记 issue 并跳过(多选/缺 reply/非法正则)", async () => {
|
|
81
|
-
const rel = await writeRules([
|
|
82
|
-
"rules:",
|
|
83
|
-
" - { key: multi, contains: a, regex: b, reply: R }", // 多选
|
|
84
|
-
" - { key: noreply, contains: c }", // 缺 reply
|
|
85
|
-
" - { key: badrx, regex: '(', reply: R }", // 非法正则
|
|
86
|
-
" - { key: ok, contains: 有效, reply: OK }",
|
|
87
|
-
].join("\n"));
|
|
88
|
-
const { config, issues } = loadReplyRules(dir, rel);
|
|
89
|
-
expect(config.rules).toHaveLength(1);
|
|
90
|
-
expect(config.rules[0].key).toBe("ok");
|
|
91
|
-
expect(issues.length).toBeGreaterThanOrEqual(3);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("stripQuotedBlock:剥离「引用消息:」块,只保留用户本轮新话", () => {
|
|
95
|
-
expect(stripQuotedBlock("引用字段是复制的内容\n\n引用消息: Hi u,你遇到的错误「所选字段形成循环引用」…"))
|
|
96
|
-
.toBe("引用字段是复制的内容");
|
|
97
|
-
// 全角冒号变体
|
|
98
|
-
expect(stripQuotedBlock("新话\n\n引用消息:旧报错文案")).toBe("新话");
|
|
99
|
-
// 无引用块 → 原文不变
|
|
100
|
-
expect(stripQuotedBlock("普通消息")).toBe("普通消息");
|
|
101
|
-
// 剥离后为空(只转发引用未说话)→ 回退原文,不制造空消息
|
|
102
|
-
const onlyQuote = "引用消息: AI字段配置异常,请检查后重试";
|
|
103
|
-
expect(stripQuotedBlock(onlyQuote)).toBe(onlyQuote);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("报错文案只在引用块里(g4-21 型追问)→ fastpath 不命中,不复读", async () => {
|
|
107
|
-
const rel = await writeRules([
|
|
108
|
-
"rules:",
|
|
109
|
-
" - { key: cycle, contains: 所选字段形成循环引用, reply: 循环引用话术 }",
|
|
110
|
-
].join("\n"));
|
|
111
|
-
const { config } = loadReplyRules(dir, rel);
|
|
112
|
-
const followUp = "@Felix 引用字段是复制的内容,不存在引用字段是其他ai生成\n\n引用消息: Hi 甘燕威,你遇到的错误「所选字段形成循环引用,数据处理异常,请检查」,以下是相关说明和解决方案:…";
|
|
113
|
-
expect(matchReplyRules(config, followUp)).toBeNull();
|
|
114
|
-
// 用户自己粘报错(无引用块)→ 仍然秒回
|
|
115
|
-
expect(matchReplyRules(config, "报错了:所选字段形成循环引用,数据处理异常,请检查"))
|
|
116
|
-
.toMatchObject({ key: "cycle" });
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("safety 关键词藏在引用块里 → 仍然拦(安全兜底不剥离,防注入藏引用)", async () => {
|
|
120
|
-
const rel = await writeRules([
|
|
121
|
-
"safety: { keywords: [涉及敏感内容], reply: 安全话术 }",
|
|
122
|
-
"rules:",
|
|
123
|
-
" - { key: any, contains: 无关, reply: 普通 }",
|
|
124
|
-
].join("\n"));
|
|
125
|
-
const { config } = loadReplyRules(dir, rel);
|
|
126
|
-
expect(matchReplyRules(config, "这条怎么回\n\n引用消息: 本内容涉及敏感内容无法生成"))
|
|
127
|
-
.toMatchObject({ key: "safety", kind: "safety" });
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe("loadClassifierRules / classifyMessage:声明式分类器 + 整档/群开关", () => {
|
|
132
|
-
let dir: string;
|
|
133
|
-
beforeEach(async () => { dir = await mkdtemp(path.join(os.tmpdir(), "sq-ts-lib-")); });
|
|
134
|
-
afterEach(async () => { await rm(dir, { recursive: true, force: true }); });
|
|
135
|
-
|
|
136
|
-
async function writeClassifiers(text: string): Promise<string> {
|
|
137
|
-
await mkdir(path.join(dir, "profile", "troubleshoot"), { recursive: true });
|
|
138
|
-
await writeFile(path.join(dir, "profile", "troubleshoot", "classifiers.yml"), text, "utf8");
|
|
139
|
-
return "troubleshoot/classifiers.yml";
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
it("未配置 / 文件缺失 → config=null", () => {
|
|
143
|
-
expect(loadClassifierRules(dir, undefined).config).toBeNull();
|
|
144
|
-
const r = loadClassifierRules(dir, "troubleshoot/missing.yml");
|
|
145
|
-
expect(r.config).toBeNull();
|
|
146
|
-
expect(r.issues.join("|")).toContain("不存在");
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("单条件 contains 命中 + extract 提取字段", async () => {
|
|
150
|
-
const rel = await writeClassifiers([
|
|
151
|
-
"classifiers:",
|
|
152
|
-
" - issueType: err",
|
|
153
|
-
" reason: 命中报错",
|
|
154
|
-
" patterns: [{ contains: ERR_X }]",
|
|
155
|
-
" extract: { code: 'ERR_X[0-9]+' }",
|
|
156
|
-
].join("\n"));
|
|
157
|
-
const { config } = loadClassifierRules(dir, rel);
|
|
158
|
-
const r = classifyMessage(config, "出现了 ERR_X42 报错");
|
|
159
|
-
expect(r).toMatchObject({ issueType: "err", reason: "命中报错" });
|
|
160
|
-
expect(r.extracted.code).toBe("ERR_X42");
|
|
161
|
-
expect(classifyMessage(config, "无关消息")).toBeNull();
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it("require_all:需全部 pattern 命中(双关键词共现)", async () => {
|
|
165
|
-
const rel = await writeClassifiers([
|
|
166
|
-
"classifiers:",
|
|
167
|
-
" - issueType: ds",
|
|
168
|
-
" require_all: true",
|
|
169
|
-
" patterns: [{ contains: docKey }, { contains: 时间戳 }]",
|
|
170
|
-
].join("\n"));
|
|
171
|
-
const { config } = loadClassifierRules(dir, rel);
|
|
172
|
-
expect(classifyMessage(config, "docKey=abc 时间戳=123")).toMatchObject({ issueType: "ds" });
|
|
173
|
-
expect(classifyMessage(config, "只有 docKey=abc")).toBeNull();
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("regex 命中 + 声明顺序返回第一个满足", async () => {
|
|
177
|
-
const rel = await writeClassifiers([
|
|
178
|
-
"classifiers:",
|
|
179
|
-
" - issueType: a",
|
|
180
|
-
" patterns: [{ regex: '错误码\\d{3}' }]",
|
|
181
|
-
" - issueType: b",
|
|
182
|
-
" patterns: [{ contains: 错误 }]",
|
|
183
|
-
].join("\n"));
|
|
184
|
-
const { config } = loadClassifierRules(dir, rel);
|
|
185
|
-
expect(classifyMessage(config, "错误码500")).toMatchObject({ issueType: "a" });
|
|
186
|
-
expect(classifyMessage(config, "只是错误描述")).toMatchObject({ issueType: "b" });
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("classifier 可从 profile 透传精确 Owner module(未声明则不带 module 字段)", async () => {
|
|
190
|
-
const rel = await writeClassifiers([
|
|
191
|
-
"classifiers:",
|
|
192
|
-
" - issueType: ai_field",
|
|
193
|
-
" module: AI字段",
|
|
194
|
-
" patterns: [{ contains: 'Sheet ID:' }]",
|
|
195
|
-
" - issueType: other",
|
|
196
|
-
" patterns: [{ contains: 其它 }]",
|
|
197
|
-
].join("\n"));
|
|
198
|
-
|
|
199
|
-
const { config } = loadClassifierRules(dir, rel);
|
|
200
|
-
expect(classifyMessage(config, "失败\nSheet ID: sht1")).toMatchObject({
|
|
201
|
-
issueType: "ai_field",
|
|
202
|
-
module: "AI字段",
|
|
203
|
-
});
|
|
204
|
-
const other = classifyMessage(config, "其它问题");
|
|
205
|
-
expect(other!.issueType).toBe("other");
|
|
206
|
-
expect(other!.module).toBeUndefined();
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it("形状非法分类器跳过 + 记 issue", async () => {
|
|
210
|
-
const rel = await writeClassifiers([
|
|
211
|
-
"classifiers:",
|
|
212
|
-
" - reason: 缺 issueType",
|
|
213
|
-
" patterns: [{ contains: x }]",
|
|
214
|
-
" - issueType: nopat",
|
|
215
|
-
" patterns: []",
|
|
216
|
-
" - issueType: ok",
|
|
217
|
-
" patterns: [{ contains: 有效 }]",
|
|
218
|
-
].join("\n"));
|
|
219
|
-
const { config, issues } = loadClassifierRules(dir, rel);
|
|
220
|
-
expect(config.classifiers).toHaveLength(1);
|
|
221
|
-
expect(config.classifiers[0].issueType).toBe("ok");
|
|
222
|
-
expect(issues.length).toBeGreaterThanOrEqual(2);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("isCapabilityEnabled:skills_dir 未配/不存在→false;存在→true(整档关闭)", async () => {
|
|
226
|
-
expect(isCapabilityEnabled(dir, undefined)).toBe(false);
|
|
227
|
-
expect(isCapabilityEnabled(dir, "skills/troubleshooting")).toBe(false);
|
|
228
|
-
await mkdir(path.join(dir, "skills", "troubleshooting"), { recursive: true });
|
|
229
|
-
expect(isCapabilityEnabled(dir, "skills/troubleshooting")).toBe(true);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it("isGroupTroubleshootingEnabled:仅明确开启的群为真", () => {
|
|
233
|
-
const groups = [{ groupId: "c1", troubleshootingEnabled: true }, { groupId: "c2", troubleshootingEnabled: false }, { groupId: "c3" }];
|
|
234
|
-
expect(isGroupToggleEnabled(groups, "c1", "troubleshootingEnabled")).toBe(true);
|
|
235
|
-
expect(isGroupToggleEnabled(groups, "c2", "troubleshootingEnabled")).toBe(false);
|
|
236
|
-
expect(isGroupToggleEnabled(groups, "c3", "troubleshootingEnabled")).toBe(false);
|
|
237
|
-
expect(isGroupToggleEnabled(groups, "cX", "troubleshootingEnabled")).toBe(false);
|
|
238
|
-
expect(isGroupToggleEnabled(undefined, "c1", "troubleshootingEnabled")).toBe(false);
|
|
239
|
-
});
|
|
240
|
-
});
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 群成员在群判定(support-qa/lib/dingtalk-group-membership)
|
|
3
|
-
*
|
|
4
|
-
* 核心不变量:**任何失败都降级为 { mode: "name" },绝不抛错**。
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
8
|
-
import {
|
|
9
|
-
resolveGroupMention,
|
|
10
|
-
resetGroupMembershipCache,
|
|
11
|
-
} from "../dingtalk-group-membership.js";
|
|
12
|
-
import { DwsClient } from "aitable-workflow-core";
|
|
13
|
-
import type { CommandRunner } from "aitable-workflow-core";
|
|
14
|
-
|
|
15
|
-
const CONTACT = { userId: "239198", openDingTalkId: "open-owner" };
|
|
16
|
-
|
|
17
|
-
/** 用注入的 dws stub 替代真实命令 */
|
|
18
|
-
function fakeDws(runJson: (args: string[]) => Promise<unknown>) {
|
|
19
|
-
return { runJson: vi.fn(async (args: string[]) => runJson(args)) };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
resetGroupMembershipCache();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe("resolveGroupMention — 判定与降级", () => {
|
|
27
|
-
it("list-by-ids 命中 → 真实 @", async () => {
|
|
28
|
-
const dws = fakeDws(async () => ({ result: { members: [{ openDingtalkId: "open-owner" }] } }));
|
|
29
|
-
await expect(resolveGroupMention("cid1", CONTACT, { dws, now: () => 1 }))
|
|
30
|
-
.resolves.toEqual({ mode: "at", userId: "239198" });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("不在群 / 非结构化响应 / 抛错 → 一律降级 name", async () => {
|
|
34
|
-
for (const response of [
|
|
35
|
-
{ result: { members: [] } },
|
|
36
|
-
{ result: {} },
|
|
37
|
-
"not-an-object",
|
|
38
|
-
null,
|
|
39
|
-
{ unexpected: true },
|
|
40
|
-
]) {
|
|
41
|
-
resetGroupMembershipCache();
|
|
42
|
-
const dws = fakeDws(async () => response);
|
|
43
|
-
await expect(resolveGroupMention("cid1", CONTACT, { dws, now: () => 1 }))
|
|
44
|
-
.resolves.toEqual({ mode: "name" });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
resetGroupMembershipCache();
|
|
48
|
-
const boom = fakeDws(async () => { throw new Error("timeout"); });
|
|
49
|
-
await expect(resolveGroupMention("cid1", CONTACT, { dws: boom, now: () => 1 }))
|
|
50
|
-
.resolves.toEqual({ mode: "name" });
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("两种 openDingTalkId 拼写都能匹配(不同 dws 版本返回不一致)", async () => {
|
|
54
|
-
for (const key of ["openDingTalkId", "openDingtalkId"]) {
|
|
55
|
-
resetGroupMembershipCache();
|
|
56
|
-
const dws = fakeDws(async () => ({ result: { members: [{ [key]: "open-owner" }] } }));
|
|
57
|
-
await expect(resolveGroupMention("cid1", CONTACT, { dws, now: () => 1 }))
|
|
58
|
-
.resolves.toEqual({ mode: "at", userId: "239198" });
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("缺 openDingTalkId / userId / 群ID → 直接降级 name 且不调 dws", async () => {
|
|
63
|
-
const dws = fakeDws(async () => ({ result: { members: [{ openDingTalkId: "open-owner" }] } }));
|
|
64
|
-
await expect(resolveGroupMention("cid1", { userId: "239198" }, { dws, now: () => 1 }))
|
|
65
|
-
.resolves.toEqual({ mode: "name" });
|
|
66
|
-
await expect(resolveGroupMention("cid1", { userId: "", openDingTalkId: "open-owner" }, { dws, now: () => 1 }))
|
|
67
|
-
.resolves.toEqual({ mode: "name" });
|
|
68
|
-
await expect(resolveGroupMention("", CONTACT, { dws, now: () => 1 }))
|
|
69
|
-
.resolves.toEqual({ mode: "name" });
|
|
70
|
-
expect(dws.runJson).not.toHaveBeenCalled();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("不手写 --format json(由 DwsClient 统一追加)", async () => {
|
|
74
|
-
let seen: string[] = [];
|
|
75
|
-
const dws = fakeDws(async (args) => { seen = args; return { result: { members: [] } }; });
|
|
76
|
-
await resolveGroupMention("cid1", CONTACT, { dws, now: () => 1 });
|
|
77
|
-
expect(seen).toEqual([
|
|
78
|
-
"chat", "group", "members", "list-by-ids", "--id", "cid1", "--users", "open-owner",
|
|
79
|
-
]);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("指定群归属企业时显式传 --profile", async () => {
|
|
83
|
-
let seen: string[] = [];
|
|
84
|
-
const dws = fakeDws(async (args) => { seen = args; return { result: { members: [] } }; });
|
|
85
|
-
await resolveGroupMention("cid1", CONTACT, { dws, corpId: " corp-b ", now: () => 1 });
|
|
86
|
-
expect(seen).toEqual([
|
|
87
|
-
"chat", "group", "members", "list-by-ids", "--id", "cid1", "--users", "open-owner",
|
|
88
|
-
"--profile", "corp-b",
|
|
89
|
-
]);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe("resolveGroupMention — 缓存", () => {
|
|
94
|
-
it("同群同人 10min 内命中缓存,过期后重新判定", async () => {
|
|
95
|
-
const dws = fakeDws(async () => ({ result: { members: [{ openDingTalkId: "open-owner" }] } }));
|
|
96
|
-
await resolveGroupMention("cid1", CONTACT, { dws, now: () => 1 });
|
|
97
|
-
await resolveGroupMention("cid1", CONTACT, { dws, now: () => 599_999 });
|
|
98
|
-
expect(dws.runJson).toHaveBeenCalledTimes(1);
|
|
99
|
-
await resolveGroupMention("cid1", CONTACT, { dws, now: () => 600_002 });
|
|
100
|
-
expect(dws.runJson).toHaveBeenCalledTimes(2);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("降级结果同样进缓存(避免故障期每条消息都重试查询)", async () => {
|
|
104
|
-
const dws = fakeDws(async () => ({ result: { members: [] } }));
|
|
105
|
-
await resolveGroupMention("cid1", CONTACT, { dws, now: () => 1 });
|
|
106
|
-
await resolveGroupMention("cid1", CONTACT, { dws, now: () => 2 });
|
|
107
|
-
expect(dws.runJson).toHaveBeenCalledTimes(1);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("不同群 / 不同人各自独立缓存", async () => {
|
|
111
|
-
const dws = fakeDws(async () => ({ result: { members: [{ openDingTalkId: "open-owner" }] } }));
|
|
112
|
-
await resolveGroupMention("cid1", CONTACT, { dws, now: () => 1 });
|
|
113
|
-
await resolveGroupMention("cid2", CONTACT, { dws, now: () => 1 });
|
|
114
|
-
await resolveGroupMention("cid1", { userId: "999", openDingTalkId: "open-other" }, { dws, now: () => 1 });
|
|
115
|
-
expect(dws.runJson).toHaveBeenCalledTimes(3);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("同群同人在不同企业 corpId 下不共享缓存", async () => {
|
|
119
|
-
const dws = fakeDws(async () => ({ result: { members: [{ openDingTalkId: "open-owner" }] } }));
|
|
120
|
-
await resolveGroupMention("cid1", CONTACT, { dws, corpId: "corp-a", now: () => 1 });
|
|
121
|
-
await resolveGroupMention("cid1", CONTACT, { dws, corpId: "corp-b", now: () => 1 });
|
|
122
|
-
expect(dws.runJson).toHaveBeenCalledTimes(2);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
describe("走 DwsClient 带来的可观测性收益", () => {
|
|
127
|
-
it("dws 首次抖动后重试仍能判出 at(迁移前会直接降级为花名)", async () => {
|
|
128
|
-
let attempts = 0;
|
|
129
|
-
const runner: CommandRunner = async () => {
|
|
130
|
-
attempts += 1;
|
|
131
|
-
if (attempts === 1) throw new Error("ETIMEDOUT");
|
|
132
|
-
return {
|
|
133
|
-
stdout: JSON.stringify({ result: { members: [{ openDingTalkId: "open-owner" }] } }),
|
|
134
|
-
stderr: "",
|
|
135
|
-
exitCode: 0,
|
|
136
|
-
};
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const dws = new DwsClient({ bin: "dws", runner });
|
|
140
|
-
await expect(resolveGroupMention("cid1", CONTACT, { dws, now: () => 1 }))
|
|
141
|
-
.resolves.toEqual({ mode: "at", userId: "239198" });
|
|
142
|
-
expect(attempts).toBeGreaterThan(1);
|
|
143
|
-
});
|
|
144
|
-
});
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 场景记忆事件工厂(support-qa/lib/memory-events)
|
|
3
|
-
*
|
|
4
|
-
* 锁定:scope 常量一致性,entry 形状与 questionKey 归一行为。
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect } from "vitest";
|
|
8
|
-
import { supportScope, supportEntry, questionKey } from "../memory-events.js";
|
|
9
|
-
|
|
10
|
-
describe("supportScope", () => {
|
|
11
|
-
it("带 conversationId 时按会话隔离", () => {
|
|
12
|
-
expect(supportScope("cid1")).toEqual({
|
|
13
|
-
channel: "dingtalk",
|
|
14
|
-
scenario: "support-qa",
|
|
15
|
-
conversationId: "cid1",
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("省略 conversationId 时为场景级 scope(长期记忆读写用)", () => {
|
|
20
|
-
expect(supportScope()).toEqual({ channel: "dingtalk", scenario: "support-qa" });
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("空串 conversationId 不写入字段(避免脏 scope 分裂记忆)", () => {
|
|
24
|
-
expect(supportScope("")).toEqual({ channel: "dingtalk", scenario: "support-qa" });
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe("supportEntry", () => {
|
|
29
|
-
it("summary 省略时按 `sender: 正文前 80 字` 生成", () => {
|
|
30
|
-
const e = supportEntry({
|
|
31
|
-
kind: "user_message",
|
|
32
|
-
content: "x".repeat(200),
|
|
33
|
-
sender: "小明",
|
|
34
|
-
role: "user",
|
|
35
|
-
conversationId: "cid1",
|
|
36
|
-
});
|
|
37
|
-
expect(e.summary).toBe(`小明: ${"x".repeat(80)}`);
|
|
38
|
-
expect(e.kind).toBe("user_message");
|
|
39
|
-
expect(e.content).toHaveLength(200);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("meta 固定带 sender/role/conversationId,且调用方 meta 可追加", () => {
|
|
43
|
-
const e = supportEntry({
|
|
44
|
-
kind: "agent_decision",
|
|
45
|
-
content: "(已决策回复)",
|
|
46
|
-
sender: "agent",
|
|
47
|
-
role: "agent",
|
|
48
|
-
conversationId: "cid9",
|
|
49
|
-
meta: { action: "reply", question: "上限多少" },
|
|
50
|
-
});
|
|
51
|
-
expect(e.meta).toEqual({
|
|
52
|
-
sender: "agent",
|
|
53
|
-
role: "agent",
|
|
54
|
-
conversationId: "cid9",
|
|
55
|
-
action: "reply",
|
|
56
|
-
question: "上限多少",
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("显式 summary / timestamp 优先", () => {
|
|
61
|
-
const e = supportEntry({
|
|
62
|
-
kind: "send_failed",
|
|
63
|
-
content: "正文",
|
|
64
|
-
sender: "agent",
|
|
65
|
-
role: "agent",
|
|
66
|
-
conversationId: "cid1",
|
|
67
|
-
summary: "发送失败(via=skipped)",
|
|
68
|
-
timestamp: "2026-08-05T00:00:00.000Z",
|
|
69
|
-
});
|
|
70
|
-
expect(e.summary).toBe("发送失败(via=skipped)");
|
|
71
|
-
expect(e.timestamp).toBe("2026-08-05T00:00:00.000Z");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("timestamp 省略时为 ISO 串", () => {
|
|
75
|
-
const e = supportEntry({
|
|
76
|
-
kind: "user_message", content: "c", sender: "s", role: "user", conversationId: "cid",
|
|
77
|
-
});
|
|
78
|
-
expect(e.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe("questionKey", () => {
|
|
83
|
-
it("折叠空白并 trim(复盘配对锚点稳定)", () => {
|
|
84
|
-
expect(questionKey(" 上限\n是\t多少 ")).toBe("上限 是 多少");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("截断到 120 字", () => {
|
|
88
|
-
expect(questionKey("问".repeat(300))).toHaveLength(120);
|
|
89
|
-
});
|
|
90
|
-
});
|