@ghyper9023/pi-dev-workflow 0.5.1 â 0.6.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/.version/RELEASE-v0.6.0.md +106 -0
- package/README.md +32 -30
- package/agents/git-agent.md +24 -16
- package/agents/grill/dev-doc-grill-agent.md +5 -4
- package/agents/grill/dev-fix-grill-agent.md +5 -3
- package/agents/grill/dev-grill-agent.md +6 -6
- package/agents/grill/dev-perf-grill-agent.md +5 -3
- package/agents/grill/dev-prd-agent.md +1 -1
- package/agents/grill/dev-refactor-grill-agent.md +5 -2
- package/agents/grill/dev-test-grill-agent.md +5 -2
- package/agents/review-agent.md +30 -22
- package/agents/workflow/docWriter-agent.md +35 -22
- package/agents/workflow/planner-agent.md +95 -57
- package/agents/workflow/reviewer-agent.md +91 -33
- package/agents/workflow/trimmer-agent.md +59 -26
- package/agents/workflow/worker-agent.md +51 -25
- package/extensions/dev-prompts.ts +29 -29
- package/extensions/grill-me-agent.ts +16 -16
- package/extensions/workflow-engine.ts +65 -239
- package/package.json +1 -1
- package/tests/test-workflow-engine-bugs.mjs +123 -9
|
@@ -399,7 +399,7 @@ const FEAT_WORKFLOW_STEPS: WorkflowStepDef[] = [
|
|
|
399
399
|
label: "ð æŽæ°ææ¡£",
|
|
400
400
|
type: "confirm",
|
|
401
401
|
agentName: "docWriter",
|
|
402
|
-
timeoutMs:
|
|
402
|
+
timeoutMs: 600_000,
|
|
403
403
|
},
|
|
404
404
|
];
|
|
405
405
|
|
|
@@ -426,7 +426,7 @@ const FIX_WORKFLOW_STEPS: WorkflowStepDef[] = [
|
|
|
426
426
|
label: "ð æŽæ°ææ¡£",
|
|
427
427
|
type: "confirm",
|
|
428
428
|
agentName: "docWriter",
|
|
429
|
-
timeoutMs:
|
|
429
|
+
timeoutMs: 600_000,
|
|
430
430
|
},
|
|
431
431
|
];
|
|
432
432
|
|
|
@@ -513,7 +513,7 @@ const DOC_WORKFLOW_STEPS: WorkflowStepDef[] = [
|
|
|
513
513
|
label: "ð æ°åææ¡£",
|
|
514
514
|
type: "auto",
|
|
515
515
|
agentName: "docWriter",
|
|
516
|
-
timeoutMs:
|
|
516
|
+
timeoutMs: 600_000,
|
|
517
517
|
},
|
|
518
518
|
];
|
|
519
519
|
|
|
@@ -536,7 +536,7 @@ const SECURITY_WORKFLOW_STEPS: WorkflowStepDef[] = [
|
|
|
536
536
|
label: "ð å®å
šå®¡æ¥",
|
|
537
537
|
type: "auto",
|
|
538
538
|
agentName: "reviewer",
|
|
539
|
-
timeoutMs:
|
|
539
|
+
timeoutMs: 900_000,
|
|
540
540
|
},
|
|
541
541
|
];
|
|
542
542
|
|
|
@@ -857,7 +857,7 @@ const COMPARE_QUESTIONS = [
|
|
|
857
857
|
export default function (pi: ExtensionAPI) {
|
|
858
858
|
// ââ /dev-feat ââââââââââââââââââââââââââââââââââââââââââââââ
|
|
859
859
|
pi.registerCommand("dev-feat", {
|
|
860
|
-
description: "(prompt wizard) æ°åèœ/åæçæ â
|
|
860
|
+
description: "(prompt wizard) æ°åèœ/åæçæ â æ¯æè®Ÿè®¡æ¹æ¡è¿œé®å®å (Grill) + èªåšåå·¥äœæµ",
|
|
861
861
|
handler: async (_args, ctx) => {
|
|
862
862
|
const answers: Record<string, string> = {};
|
|
863
863
|
let featIdx = 0;
|
|
@@ -911,17 +911,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
911
911
|
|
|
912
912
|
// ââ /dev-fix âââââââââââââââââââââââââââââââââââââââââââââââ
|
|
913
913
|
pi.registerCommand("dev-fix", {
|
|
914
|
-
description: "(prompt wizard) é®é¢ææ¥/éè¯¯ä¿®æ£ â
|
|
914
|
+
description: "(prompt wizard) é®é¢ææ¥/éè¯¯ä¿®æ£ â æ¯æ Bug æ ¹å è¿œé® (Grill)",
|
|
915
915
|
handler: async (_args, ctx) => {
|
|
916
916
|
await runWizardWithGrill(
|
|
917
917
|
ctx, pi, "fix", "é®é¢ææ¥/é误修æ£",
|
|
918
918
|
FIX_QUESTIONS, assembleFixPrompt,
|
|
919
919
|
{
|
|
920
920
|
agentDef: _fixGrillAgent,
|
|
921
|
-
title: "ð Bug
|
|
922
|
-
description: "AI
|
|
921
|
+
title: "ð Bug æ ¹å 远é®",
|
|
922
|
+
description: "AI äŒéè¿äžæ¥æ¥è¿œé®åž®äœ 粟åå®äœæ ¹å ïŒä»å€ç°æ¡ä»¶å°æ ¹æ¬åå æšçïŒåå°ä¿®å€æ¹æ¡éªè¯åååœé£é©è¯äŒ°ã",
|
|
923
923
|
questionTitle: "Bug æ ¹å åæ",
|
|
924
|
-
loaderLabel: "ð§ AI
|
|
924
|
+
loaderLabel: "ð§ AI æ£åšåæä»£ç å¹¶çææ ¹å 远é®é®é¢...",
|
|
925
925
|
},
|
|
926
926
|
{ steps: FIX_WORKFLOW_STEPS },
|
|
927
927
|
);
|
|
@@ -930,17 +930,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
930
930
|
|
|
931
931
|
// ââ /dev-doc âââââââââââââââââââââââââââââââââââââââââââââââ
|
|
932
932
|
pi.registerCommand("dev-doc", {
|
|
933
|
-
description: "(prompt wizard) ææ¡£çæ/æ»ç» â
|
|
933
|
+
description: "(prompt wizard) ææ¡£çæ/æ»ç» â æ¯æææ¡£å€§çº²è¿œé®å®å (Grill)",
|
|
934
934
|
handler: async (_args, ctx) => {
|
|
935
935
|
await runWizardWithGrill(
|
|
936
936
|
ctx, pi, "doc", "ææ¡£çæ/æ»ç»",
|
|
937
937
|
DOC_QUESTIONS, assembleDocPrompt,
|
|
938
938
|
{
|
|
939
939
|
agentDef: _docGrillAgent,
|
|
940
|
-
title: "ð
|
|
941
|
-
description: "AI
|
|
942
|
-
questionTitle: "
|
|
943
|
-
loaderLabel: "ð§ AI
|
|
940
|
+
title: "ð ææ¡£å€§çº²è¿œé®å®å",
|
|
941
|
+
description: "AI äŒéè¿è¿œé®åž®äœ å®åææ¡£å€§çº²ïŒä»åäŒå®äœå°ç»æå®æïŒç¡®è®€æ¯è¯äžèŽæ§å瀺äŸèŠçèåŽã",
|
|
942
|
+
questionTitle: "ææ¡£å€§çº²è¿œé®å®å",
|
|
943
|
+
loaderLabel: "ð§ AI æ£åšåæå¹¶çæææ¡£å€§çº²è¿œé®é®é¢...",
|
|
944
944
|
},
|
|
945
945
|
{ steps: DOC_WORKFLOW_STEPS },
|
|
946
946
|
);
|
|
@@ -949,17 +949,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
949
949
|
|
|
950
950
|
// ââ /dev-refactor ââââââââââââââââââââââââââââââââââââââââââ
|
|
951
951
|
pi.registerCommand("dev-refactor", {
|
|
952
|
-
description: "(prompt wizard) éæ/äŒåç°æç»æ â
|
|
952
|
+
description: "(prompt wizard) éæ/äŒåç°æç»æ â æ¯æéææ¹æ¡è¿œé® (Grill)",
|
|
953
953
|
handler: async (_args, ctx) => {
|
|
954
954
|
await runWizardWithGrill(
|
|
955
955
|
ctx, pi, "refactor", "éæ/äŒå",
|
|
956
956
|
REFACTOR_QUESTIONS, assembleRefactorPrompt,
|
|
957
957
|
{
|
|
958
958
|
agentDef: _refactorGrillAgent,
|
|
959
|
-
title: "ð§
|
|
960
|
-
description: "AI
|
|
961
|
-
questionTitle: "
|
|
962
|
-
loaderLabel: "ð§ AI
|
|
959
|
+
title: "ð§ éææ¹æ¡è¿œé®",
|
|
960
|
+
description: "AI äŒéè¿è¿œé®åž®äœ è¯å«éèèŠåé£é©ïŒä»æš¡å蟹çå° API å
Œå®¹æ§ïŒéªè¯è¡äžºä¿æåè¿ç§»è·¯åŸå®å
šæ§ã",
|
|
961
|
+
questionTitle: "éææ¹æ¡è¿œé®",
|
|
962
|
+
loaderLabel: "ð§ AI æ£åšåæä»£ç å¹¶çæéæè¿œé®é®é¢...",
|
|
963
963
|
},
|
|
964
964
|
{ steps: REFACTOR_WORKFLOW_STEPS },
|
|
965
965
|
);
|
|
@@ -968,17 +968,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
968
968
|
|
|
969
969
|
// ââ /dev-test ââââââââââââââââââââââââââââââââââââââââââââââ
|
|
970
970
|
pi.registerCommand("dev-test", {
|
|
971
|
-
description: "(prompt wizard) æµè¯çšäŸçæ â
|
|
971
|
+
description: "(prompt wizard) æµè¯çšäŸçæ â æ¯ææµè¯çç¥è¿œé® (Grill)",
|
|
972
972
|
handler: async (_args, ctx) => {
|
|
973
973
|
await runWizardWithGrill(
|
|
974
974
|
ctx, pi, "test", "æµè¯çšäŸ/è¯äŒ°",
|
|
975
975
|
TEST_QUESTIONS, assembleTestPrompt,
|
|
976
976
|
{
|
|
977
977
|
agentDef: _testGrillAgent,
|
|
978
|
-
title: "ð§ª
|
|
979
|
-
description: "AI
|
|
980
|
-
questionTitle: "
|
|
981
|
-
loaderLabel: "ð§ AI
|
|
978
|
+
title: "𧪠æµè¯çç¥è¿œé®",
|
|
979
|
+
description: "AI äŒéè¿è¿œé®åž®äœ åç°æµè¯çŒºå£ïŒä»èŠç绎床å°èŸ¹çæ¡ä»¶ïŒéªè¯æš¡æçç¥åæµè¯é犻æ¯åŠå°äœã",
|
|
980
|
+
questionTitle: "æµè¯çç¥è¿œé®",
|
|
981
|
+
loaderLabel: "ð§ AI æ£åšåæå¹¶çææµè¯è¿œé®é®é¢...",
|
|
982
982
|
},
|
|
983
983
|
{ steps: TEST_WORKFLOW_STEPS },
|
|
984
984
|
);
|
|
@@ -995,17 +995,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
995
995
|
|
|
996
996
|
// ââ /dev-perf ââââââââââââââââââââââââââââââââââââââââââââââ
|
|
997
997
|
pi.registerCommand("dev-perf", {
|
|
998
|
-
description: "(prompt wizard) æ§èœäŒå â
|
|
998
|
+
description: "(prompt wizard) æ§èœäŒå â æ¯ææ§èœäŒåæ¹æ¡è¿œé® (Grill)",
|
|
999
999
|
handler: async (_args, ctx) => {
|
|
1000
1000
|
await runWizardWithGrill(
|
|
1001
1001
|
ctx, pi, "perf", "æ§èœäŒå",
|
|
1002
1002
|
PERF_QUESTIONS, assemblePerfPrompt,
|
|
1003
1003
|
{
|
|
1004
1004
|
agentDef: _perfGrillAgent,
|
|
1005
|
-
title: "â¡
|
|
1006
|
-
description: "AI
|
|
1007
|
-
questionTitle: "
|
|
1008
|
-
loaderLabel: "ð§ AI
|
|
1005
|
+
title: "â¡ æ§èœäŒåæ¹æ¡è¿œé®",
|
|
1006
|
+
description: "AI äŒéè¿è¿œé®åž®äœ éªè¯ç¶é¢å€æåäŒåæ¹åïŒä»åºåæµè¯æ¹æ³å°æœåšååœé£é©ïŒç¡®ä¿æ¹æ¡åçæ§ã",
|
|
1007
|
+
questionTitle: "æ§èœäŒåæ¹æ¡è¿œé®",
|
|
1008
|
+
loaderLabel: "ð§ AI æ£åšåæå¹¶çææ§èœäŒå远é®é®é¢...",
|
|
1009
1009
|
},
|
|
1010
1010
|
{ steps: PERF_WORKFLOW_STEPS },
|
|
1011
1011
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* grill-me-agent.ts â
|
|
2
|
+
* grill-me-agent.ts â 讟计 (Grill) å PRD çæçç¬ç«ç®¡çåš
|
|
3
3
|
*
|
|
4
4
|
* è莣ïŒ
|
|
5
5
|
* 1. runGrillPhase() â å¯åš sub-agent çæè¯å®¡é®é¢ïŒTUI éé¢åç°ïŒé项 + èªå®ä¹èŸå
¥ïŒ
|
|
@@ -372,10 +372,10 @@ export async function runGrillPhase(
|
|
|
372
372
|
};
|
|
373
373
|
|
|
374
374
|
const agentDef = options?.agentDef ?? _defaultGrillAgent;
|
|
375
|
-
const confirmTitle = options?.title ?? "ð
|
|
376
|
-
const confirmDesc = options?.description ?? "AI
|
|
377
|
-
const qTitlePrefix = options?.questionTitle ?? "
|
|
378
|
-
const loaderLabel = options?.loaderLabel ?? "ð§ AI
|
|
375
|
+
const confirmTitle = options?.title ?? "ð è®Ÿè®¡æ¹æ¡è¿œé®å®å";
|
|
376
|
+
const confirmDesc = options?.description ?? "AI äŒéè¿ç³»ç»æ§è¿œé®åž®äœ æç£šæ¹æ¡ïŒä»æ¯è¯ç²Ÿç¡®åå°èŸ¹çæ¡ä»¶éªè¯ïŒç¡®ä¿æ¶æå³ççæ¯äžªåæ¯éœç»è¿æšæ²ã";
|
|
377
|
+
const qTitlePrefix = options?.questionTitle ?? "è®Ÿè®¡æ¹æ¡è¿œé®å®å";
|
|
378
|
+
const loaderLabel = options?.loaderLabel ?? "ð§ AI åä»£çæ£åšåæä»£ç å¹¶çæè¿œé®é®é¢...";
|
|
379
379
|
|
|
380
380
|
// ââ Step 1: Confirm entering grill mode ââââââââââââââââââ
|
|
381
381
|
const enterGrill = await uiConfirm(ctx, confirmTitle, confirmDesc);
|
|
@@ -438,16 +438,16 @@ export async function runGrillPhase(
|
|
|
438
438
|
|
|
439
439
|
const choice = await uiSelect(
|
|
440
440
|
ctx,
|
|
441
|
-
"â ïž AI
|
|
441
|
+
"â ïž AI æªèœæåçæè¿œé®é®é¢",
|
|
442
442
|
[
|
|
443
|
-
"ð
|
|
443
|
+
"ð éæ°å°è¯çæè¿œé®é®é¢",
|
|
444
444
|
"âïž è·³è¿ GrillïŒçŽæ¥åé Prompt",
|
|
445
445
|
"â åæ¶ (Esc)",
|
|
446
446
|
],
|
|
447
447
|
);
|
|
448
448
|
|
|
449
449
|
switch (choice) {
|
|
450
|
-
case "ð
|
|
450
|
+
case "ð éæ°å°è¯çæè¿œé®é®é¢": {
|
|
451
451
|
const retryPath = grillOutputPath(ctx.cwd);
|
|
452
452
|
const errorFeedback = parseErrorMsg
|
|
453
453
|
? [
|
|
@@ -513,13 +513,13 @@ export async function runGrillPhase(
|
|
|
513
513
|
rIdx++;
|
|
514
514
|
}
|
|
515
515
|
const qaBlock = pairs
|
|
516
|
-
.map((p, i) => `[
|
|
516
|
+
.map((p, i) => `[远é®é®é¢ ${i + 1}]\né®é¢: ${p.question}\nåç: ${p.answer}`)
|
|
517
517
|
.join("\n\n");
|
|
518
518
|
const finalEnhancedPrompt = [
|
|
519
519
|
assembledPrompt,
|
|
520
520
|
"",
|
|
521
521
|
"---",
|
|
522
|
-
"##
|
|
522
|
+
"## 讟计远é®è®°åœ",
|
|
523
523
|
"",
|
|
524
524
|
"ä»¥äžæ¯åšåŒååè¿è¡ç讟计è¯å®¡é®çïŒææå³ç已确讀ïŒ",
|
|
525
525
|
"",
|
|
@@ -572,16 +572,16 @@ export async function runGrillPhase(
|
|
|
572
572
|
|
|
573
573
|
// ââ Step 6: Assemble enhanced prompt âââââââââââââââââââââ
|
|
574
574
|
const qaBlock = pairs
|
|
575
|
-
.map((p, i) => `[
|
|
575
|
+
.map((p, i) => `[远é®é®é¢ ${i + 1}]\né®é¢: ${p.question}\nåç: ${p.answer}`)
|
|
576
576
|
.join("\n\n");
|
|
577
577
|
|
|
578
578
|
const finalEnhancedPrompt = [
|
|
579
579
|
assembledPrompt,
|
|
580
580
|
"",
|
|
581
581
|
"---",
|
|
582
|
-
"##
|
|
582
|
+
"## æ¹æ¡è¿œé®è®°åœ",
|
|
583
583
|
"",
|
|
584
|
-
"
|
|
584
|
+
"ä»¥äžæ¯åšåŒååè¿è¡ç远é®åŒæ¹æ¡æç£šè®°åœïŒææå³ç已确讀ïŒ",
|
|
585
585
|
"",
|
|
586
586
|
qaBlock,
|
|
587
587
|
].join("\n");
|
|
@@ -615,7 +615,7 @@ async function showQuestionTUI(
|
|
|
615
615
|
q: GrillQuestion,
|
|
616
616
|
currentIndex: number,
|
|
617
617
|
totalCount: number,
|
|
618
|
-
titlePrefix = "
|
|
618
|
+
titlePrefix = "è®Ÿè®¡æ¹æ¡è¿œé®å®å",
|
|
619
619
|
backable = false,
|
|
620
620
|
previousAnswer?: string,
|
|
621
621
|
): Promise<string | null> {
|
|
@@ -707,8 +707,8 @@ async function showQuestionTUI(
|
|
|
707
707
|
|
|
708
708
|
container.addChild(new Spacer(1));
|
|
709
709
|
const hint = backable && currentIndex > 1
|
|
710
|
-
? " ââ å¯Œèª â¢ Enter éæ© ⢠Ctrl+Shift+â è¿åäžäžé¢ ⢠Esc
|
|
711
|
-
: " ââ å¯Œèª â¢ Enter éæ© ⢠Esc
|
|
710
|
+
? " ââ å¯Œèª â¢ Enter éæ© ⢠Ctrl+Shift+â è¿åäžäžé¢ ⢠Esc åæ¶å
šéšè¿œé®"
|
|
711
|
+
: " ââ å¯Œèª â¢ Enter éæ© ⢠Esc åæ¶å
šéšè¿œé®";
|
|
712
712
|
container.addChild(
|
|
713
713
|
new Text(theme.fg("dim", hint), 0, 0),
|
|
714
714
|
);
|
|
@@ -447,13 +447,19 @@ function captureBaseline(cwd: string): void {
|
|
|
447
447
|
*
|
|
448
448
|
* Uses git-format status codes (M, A, D) for display consistency.
|
|
449
449
|
* Deduplicates against existing _workflowFileChanges.
|
|
450
|
+
*
|
|
451
|
+
* The dedup key includes stepIndex (filePath:stepIndex) so the same file
|
|
452
|
+
* can appear across different workflow steps (e.g., created by worker in
|
|
453
|
+
* loop-group, then reviewed by reviewer) without being collapsed into one.
|
|
454
|
+
* Without :stepIndex, the Set dedup would wrongly skip a file modified in
|
|
455
|
+
* a later step just because it was already tracked in an earlier step.
|
|
450
456
|
*/
|
|
451
457
|
function updateToolsFromGit(cwd: string, stepIndex: number, agentName: string): void {
|
|
452
458
|
const currentChanges = getGitDiffChanges(cwd);
|
|
453
|
-
const seen = new Set(_workflowFileChanges.map(c => c.filePath));
|
|
454
|
-
|
|
459
|
+
const seen = new Set(_workflowFileChanges.map(c => `${c.filePath}:${c.stepIndex}`));
|
|
460
|
+
|
|
455
461
|
for (const change of currentChanges) {
|
|
456
|
-
if (seen.has(change.path)) continue;
|
|
462
|
+
if (seen.has(`${change.path}:${stepIndex}`)) continue;
|
|
457
463
|
|
|
458
464
|
// ââ Baseline filtering ââââââââââââââââââââââââââââââ
|
|
459
465
|
// Skip files that were already dirty at workflow start and haven't been touched.
|
|
@@ -470,7 +476,13 @@ function updateToolsFromGit(cwd: string, stepIndex: number, agentName: string):
|
|
|
470
476
|
}
|
|
471
477
|
}
|
|
472
478
|
|
|
473
|
-
|
|
479
|
+
// Skip files in .pi-dev-output/ â these are workflow-generated artifacts (plans, reviews)
|
|
480
|
+
// and should NOT be reported as user repo changes. Real git clients (VSCode, Zed) also
|
|
481
|
+
// ignore this directory (it's in .gitignore). This filter is defense-in-depth.
|
|
482
|
+
// Note: git always uses forward slashes in paths, even on Windows.
|
|
483
|
+
if (change.path.startsWith(".pi-dev-output/")) continue;
|
|
484
|
+
|
|
485
|
+
seen.add(`${change.path}:${stepIndex}`);
|
|
474
486
|
const type: FileChangeEntry["type"] =
|
|
475
487
|
change.status === "A" ? "new" :
|
|
476
488
|
change.status === "D" ? "delete" :
|
|
@@ -883,47 +895,6 @@ function addWidgetSubStepTool(stepIndex: number, agentName: string, tool: string
|
|
|
883
895
|
if (!sub.tools) sub.tools = [];
|
|
884
896
|
sub.tools.push(tool);
|
|
885
897
|
if (sub.tools.length > 20) sub.tools = sub.tools.slice(-20); // keep last 20
|
|
886
|
-
|
|
887
|
-
// Also track as file change for checkpoint
|
|
888
|
-
// Support both old format ("edit: path") and new git-format ("M path", "A path", "D path")
|
|
889
|
-
const oldMatch = tool.match(/^(edit|new|delete|read):\s*(.+)/i);
|
|
890
|
-
const gitMatch = !oldMatch ? tool.match(/^([MAD])\s{2,}(.+)$/) : null;
|
|
891
|
-
if (oldMatch) {
|
|
892
|
-
const changeType = oldMatch[1]!.toLowerCase() as FileChangeEntry["type"];
|
|
893
|
-
const filePath = oldMatch[2]!.trim();
|
|
894
|
-
const exists = _workflowFileChanges.some(
|
|
895
|
-
c => c.filePath === filePath && c.type === changeType && c.stepIndex === stepIndex && c.agent === agentName,
|
|
896
|
-
);
|
|
897
|
-
if (!exists && filePath.length > 3) {
|
|
898
|
-
_workflowFileChanges.push({
|
|
899
|
-
agent: agentName,
|
|
900
|
-
stepIndex,
|
|
901
|
-
type: changeType,
|
|
902
|
-
filePath,
|
|
903
|
-
timestamp: new Date().toISOString(),
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
} else if (gitMatch) {
|
|
907
|
-
const gitStatus = gitMatch[1]!;
|
|
908
|
-
const changeType: FileChangeEntry["type"] =
|
|
909
|
-
gitStatus === "A" ? "new" :
|
|
910
|
-
gitStatus === "D" ? "delete" :
|
|
911
|
-
"edit";
|
|
912
|
-
const filePath = gitMatch[2]!.trim();
|
|
913
|
-
const exists = _workflowFileChanges.some(
|
|
914
|
-
c => c.filePath === filePath && c.type === changeType && c.stepIndex === stepIndex && c.agent === agentName,
|
|
915
|
-
);
|
|
916
|
-
if (!exists && filePath.length > 3) {
|
|
917
|
-
_workflowFileChanges.push({
|
|
918
|
-
agent: agentName,
|
|
919
|
-
stepIndex,
|
|
920
|
-
type: changeType,
|
|
921
|
-
filePath,
|
|
922
|
-
timestamp: new Date().toISOString(),
|
|
923
|
-
});
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
|
|
927
898
|
refreshWidget();
|
|
928
899
|
}
|
|
929
900
|
}
|
|
@@ -1130,7 +1101,7 @@ async function runAgentWithProgress(
|
|
|
1130
1101
|
const result = await spawnSubagent(agent, task, _workflowCwd, signal, timeoutMs, (progress) => {
|
|
1131
1102
|
// Try to parse tool calls from progress messages
|
|
1132
1103
|
// Only match if it looks like a file path (contains a dot or path separator)
|
|
1133
|
-
const toolMatch = progress.match(/(edit|read|write|new|bash|grep|find|ls|delete|remove)\s*[:ïŒ]\s*(\
|
|
1104
|
+
const toolMatch = progress.match(/(edit|read|write|new|bash|grep|find|ls|delete|remove)\s*[:ïŒ]\s*([\w./\\-]+)/i);
|
|
1134
1105
|
if (toolMatch) {
|
|
1135
1106
|
const toolType = toolMatch[1]!.toLowerCase();
|
|
1136
1107
|
const target = toolMatch[2]!;
|
|
@@ -1141,12 +1112,14 @@ async function runAgentWithProgress(
|
|
|
1141
1112
|
_widgetExtraToolCount++;
|
|
1142
1113
|
}
|
|
1143
1114
|
}
|
|
1144
|
-
// Detect output file paths â
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1115
|
+
// Detect output file paths â only match .pi-dev-output paths belonging to current workflow
|
|
1116
|
+
if (_workflowId) {
|
|
1117
|
+
const outputMatch = progress.match(/\.pi-dev-output\/[^\s,;)\]}'"`]{5,}\.\w+/i);
|
|
1118
|
+
if (outputMatch) {
|
|
1119
|
+
const pathCandidate = outputMatch[0]!.trim().replace(/["'`\\]+$/g, '');
|
|
1120
|
+
if (pathCandidate.length > 15 && pathCandidate.length < 300 && pathCandidate.includes(_workflowId)) {
|
|
1121
|
+
addWidgetSubStepOutput(stepIndex, agentName, pathCandidate);
|
|
1122
|
+
}
|
|
1150
1123
|
}
|
|
1151
1124
|
}
|
|
1152
1125
|
}, _workflowId ? { workflowId: _workflowId } : undefined);
|
|
@@ -1163,127 +1136,47 @@ async function runAgentWithProgress(
|
|
|
1163
1136
|
toolCount: _widgetExtraToolCount,
|
|
1164
1137
|
});
|
|
1165
1138
|
|
|
1166
|
-
// ââ Post-completion:
|
|
1167
|
-
//
|
|
1168
|
-
//
|
|
1139
|
+
// ââ Post-completion: file change detection via git diff ONLY ââ
|
|
1140
|
+
// We do NOT parse agent output text for file paths â that approach is fragile and
|
|
1141
|
+
// produces false positives (e.g., matching JS property names like `.push`, `.length`
|
|
1142
|
+
// from code snippets). Instead, we rely entirely on git diff --name-status, which is
|
|
1143
|
+
// the same approach used by VSCode, Zed, and every other professional git client.
|
|
1144
|
+
// It's deterministic, noise-free, and always produces correct relative paths.
|
|
1169
1145
|
const allOutput = (result.output || "") + "\n" + (result.stderr || "");
|
|
1170
1146
|
const finalOutput = extractFinalOutput(result.output) || result.output;
|
|
1171
1147
|
const searchText = allOutput + "\n" + finalOutput;
|
|
1172
1148
|
|
|
1173
|
-
//
|
|
1174
|
-
//
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
// Chinese patterns
|
|
1183
|
-
/(?:çŒå|å建|ä¿®æ¹|å é€|读å|åå
¥|æŽæ°)\s*(?:äº|æä»¶)?\s*[:ïŒ]?\s*[`"']?([^`"'\s,ïŒ]+\.[a-zA-Z0-9_]+)[`"']?/gi,
|
|
1184
|
-
// File path with action prefix: "edit: src/file.ts", "new: src/file.ts"
|
|
1185
|
-
/(?:^|\n)\s*(?:edit|new|delete|read|modify|create|update|add|remove)\s*[:ïŒ]\s*([^\n]+\.[a-zA-Z0-9_]+)/gim,
|
|
1186
|
-
];
|
|
1187
|
-
const seenTools = new Set<string>();
|
|
1188
|
-
for (const pattern of filePatterns) {
|
|
1149
|
+
// Find output file paths (plans, reviews) for display in the "output:" section
|
|
1150
|
+
// These are workflow artifacts (.pi-dev-output/) â only show files belonging to
|
|
1151
|
+
// the CURRENT workflow to avoid cross-contamination from previous runs.
|
|
1152
|
+
// Note: file change detection itself relies exclusively on git diff below, not text parsing.
|
|
1153
|
+
const seenOutputs = new Set<string>();
|
|
1154
|
+
if (_workflowId) {
|
|
1155
|
+
// Only match .pi-dev-output paths containing the current workflow UUID
|
|
1156
|
+
const escapedId = _workflowId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1157
|
+
const outputPattern = new RegExp(`\\.pi-dev-output\\/[^\\s,;)\\]}'"\\`]*${escapedId}[^\\s,;)\\]}'"\\`]*`, 'g');
|
|
1189
1158
|
let m;
|
|
1190
|
-
while ((m =
|
|
1191
|
-
const
|
|
1192
|
-
|
|
1193
|
-
.
|
|
1194
|
-
|
|
1195
|
-
// Validate it's a real file path
|
|
1196
|
-
if (filePath.length > 3 && filePath.length < 300 && !seenTools.has(filePath)) {
|
|
1197
|
-
// Skip common non-file matches
|
|
1198
|
-
if (filePath.match(/^(the|a|an|this|that|it|its|my|your|our|their|some|any|all|each|every|both|few|many|several|most|other|another|such|what|which|whose|whom|when|where|why|how|who|being|having|doing|making|taking|giving|getting|setting|using|running|going|coming|looking|finding|keeping|putting)/i)) continue;
|
|
1199
|
-
if (filePath.startsWith("http")) continue;
|
|
1200
|
-
if (filePath.length < 6 && !filePath.includes("/")) continue;
|
|
1201
|
-
|
|
1202
|
-
// é¢å€è¿æ»€åšïŒæé€ææŸäžæ¯æä»¶è·¯åŸçèæ°æ®
|
|
1203
|
-
if (filePath.includes("${") || filePath.includes("\\n") || filePath.includes("\\t")) continue; // æé€æš¡æ¿å笊䞲å蜬ä¹å笊
|
|
1204
|
-
if (filePath.includes("[]") || filePath.includes("{}")) continue; // æé€æ°ç»/对象åé¢é
|
|
1205
|
-
if (filePath.match(/^[\s,;)\]}]+$/)) continue; // æé€çº¯ç¬Šå·
|
|
1206
|
-
seenTools.add(filePath);
|
|
1207
|
-
const fullMatch = m[0]!.toLowerCase();
|
|
1208
|
-
// Determine operation type and convert to git status
|
|
1209
|
-
let toolType = "edit";
|
|
1210
|
-
if (fullMatch.includes("write") || fullMatch.includes("å建") || fullMatch.includes("new") || fullMatch.includes("add") || fullMatch.includes("created") || fullMatch.includes("added")) {
|
|
1211
|
-
toolType = "new";
|
|
1212
|
-
} else if (fullMatch.includes("delete") || fullMatch.includes("å é€") || fullMatch.includes("remove") || fullMatch.includes("deleted") || fullMatch.includes("removed")) {
|
|
1213
|
-
toolType = "delete";
|
|
1214
|
-
} else if (fullMatch.includes("read") || fullMatch.includes("读å")) {
|
|
1215
|
-
toolType = "read";
|
|
1216
|
-
}
|
|
1217
|
-
if (toolType !== "read") {
|
|
1218
|
-
const gitStatus = toGitStatus(toolType);
|
|
1219
|
-
addWidgetSubStepTool(stepIndex, agentName, `${gitStatus} ${filePath}`);
|
|
1220
|
-
_widgetExtraToolCount++;
|
|
1221
|
-
}
|
|
1159
|
+
while ((m = outputPattern.exec(searchText)) !== null) {
|
|
1160
|
+
const path_ = m[0]!.trim();
|
|
1161
|
+
if (path_.length > 15 && path_.length < 300 && !seenOutputs.has(path_)) {
|
|
1162
|
+
seenOutputs.add(path_);
|
|
1163
|
+
addWidgetSubStepOutput(stepIndex, agentName, path_);
|
|
1222
1164
|
}
|
|
1223
1165
|
}
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
// Look for explicit tool call patterns in the raw JSON output
|
|
1228
|
-
if (seenTools.size === 0) {
|
|
1229
|
-
const jsonLines = (result.output || "").split("\n");
|
|
1230
|
-
for (const line of jsonLines) {
|
|
1231
|
-
try {
|
|
1232
|
-
const event = JSON.parse(line);
|
|
1233
|
-
// Look for tool_use events in the JSON stream
|
|
1234
|
-
if (event.type === "message_update" && event.assistantMessageEvent?.type === "tool_use") {
|
|
1235
|
-
const toolName = event.assistantMessageEvent.name;
|
|
1236
|
-
const args = event.assistantMessageEvent.args || {};
|
|
1237
|
-
// write tool: args contains file_path
|
|
1238
|
-
if (toolName === "write" && args.file_path) {
|
|
1239
|
-
const fp = args.file_path.trim();
|
|
1240
|
-
if (!seenTools.has(fp)) {
|
|
1241
|
-
seenTools.add(fp);
|
|
1242
|
-
addWidgetSubStepTool(stepIndex, agentName, `A ${fp}`);
|
|
1243
|
-
_widgetExtraToolCount++;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
// edit tool: args contains file_path
|
|
1247
|
-
if (toolName === "edit" && args.file_path) {
|
|
1248
|
-
const fp = args.file_path.trim();
|
|
1249
|
-
if (!seenTools.has(fp)) {
|
|
1250
|
-
seenTools.add(fp);
|
|
1251
|
-
addWidgetSubStepTool(stepIndex, agentName, `M ${fp}`);
|
|
1252
|
-
_widgetExtraToolCount++;
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
} catch { /* not JSON, skip */ }
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
// Find output file paths (.pi-dev-output, review reports, plan files)
|
|
1261
|
-
// NOTE: Only match explicit paths in .pi-dev-output/ or known review/plan file patterns.
|
|
1262
|
-
// The old pattern matching loose "output:" text was the root cause of "output::0.00004508" noise.
|
|
1263
|
-
// File change detection now relies on git diff --name-status (updateToolsFromGit below),
|
|
1264
|
-
// which is deterministic and noise-free.
|
|
1265
|
-
const outputPathPatterns = [
|
|
1266
|
-
// Direct reference to .pi-dev-output paths: ".pi-dev-output/pi-plans/xxx.md"
|
|
1267
|
-
/\.pi-dev-output\/[a-zA-Z0-9_\/-]+\.[a-zA-Z0-9]+/g,
|
|
1268
|
-
// Review file patterns: "review-20260520-162800.md"
|
|
1269
|
-
/review-\d{8}-\d{6}\.md/g,
|
|
1270
|
-
// Plan file patterns: "20260520-1628-*.md"
|
|
1271
|
-
/\d{8}-\d{4,6}-[a-zA-Z0-9_-]+\.md/g,
|
|
1272
|
-
];
|
|
1273
|
-
const seenOutputs = new Set<string>();
|
|
1274
|
-
for (const pattern of outputPathPatterns) {
|
|
1166
|
+
} else {
|
|
1167
|
+
// No workflow ID (unlikely): match .pi-dev-output paths conservatively
|
|
1168
|
+
const outputPattern = /\.pi-dev-output\/[a-zA-Z0-9_\/-]+\.[a-zA-Z0-9]+/g;
|
|
1275
1169
|
let m;
|
|
1276
|
-
while ((m =
|
|
1170
|
+
while ((m = outputPattern.exec(searchText)) !== null) {
|
|
1277
1171
|
const path_ = m[0]!.trim();
|
|
1278
|
-
if (path_.length >
|
|
1172
|
+
if (path_.length > 15 && path_.length < 300 && !seenOutputs.has(path_)) {
|
|
1279
1173
|
seenOutputs.add(path_);
|
|
1280
1174
|
addWidgetSubStepOutput(stepIndex, agentName, path_);
|
|
1281
1175
|
}
|
|
1282
1176
|
}
|
|
1283
1177
|
}
|
|
1284
1178
|
|
|
1285
|
-
|
|
1286
|
-
// ââ Update file changes from git diff (more accurate than text scraping) ââ
|
|
1179
|
+
// ââ Update file changes from git diff (more accurate than text scraping) ââ
|
|
1287
1180
|
updateToolsFromGit(_workflowCwd, stepIndex, agentName);
|
|
1288
1181
|
// Update sub-step status based on result
|
|
1289
1182
|
const subStatus: WorkflowSubStepWidgetState["status"] =
|
|
@@ -1350,41 +1243,19 @@ async function executeSingleStep(
|
|
|
1350
1243
|
throw new Error(`Agent é误 (exit ${result.exitCode}): ${result.stderr.slice(0, 500)}`);
|
|
1351
1244
|
}
|
|
1352
1245
|
|
|
1353
|
-
// ââ Capture chain context for single-step agents ââ
|
|
1354
|
-
const agentChanges = _workflowFileChanges
|
|
1355
|
-
.filter(c => c.stepIndex === stepIndex && c.agent === agentName)
|
|
1356
|
-
.map(c => `${c.type === "new" ? "A" : c.type === "delete" ? "D" : "M"} ${c.filePath}`);
|
|
1357
|
-
|
|
1358
|
-
let chainKey: string;
|
|
1359
|
-
if (agentName === "planner") chainKey = "计åå¶å®æèŠ";
|
|
1360
|
-
else if (agentName === "docWriter") chainKey = "ææ¡£æŽæ°æèŠ";
|
|
1361
|
-
else chainKey = `${agentName} æ§è¡æèŠ`;
|
|
1362
|
-
|
|
1363
|
-
if (agentChanges.length > 0) {
|
|
1364
|
-
const editCount = agentChanges.filter(c => c.startsWith("M")).length;
|
|
1365
|
-
const newCount = agentChanges.filter(c => c.startsWith("A")).length;
|
|
1366
|
-
const delCount = agentChanges.filter(c => c.startsWith("D")).length;
|
|
1367
|
-
const statsParts: string[] = [];
|
|
1368
|
-
if (editCount > 0) statsParts.push(`ä¿®æ¹ ${editCount} 䞪`);
|
|
1369
|
-
if (newCount > 0) statsParts.push(`æ°å¢ ${newCount} 䞪`);
|
|
1370
|
-
if (delCount > 0) statsParts.push(`å é€ ${delCount} 䞪`);
|
|
1371
|
-
const statsLine = statsParts.length > 0 ? `æ¹åšç»è®¡: ${statsParts.join("ïŒ")}\n\n` : "";
|
|
1372
|
-
|
|
1373
|
-
updateChainContext(chainKey,
|
|
1374
|
-
`${agentName} 已宿ã\n` +
|
|
1375
|
-
statsLine +
|
|
1376
|
-
`åæŽæä»¶å衚:\n${agentChanges.join("\n")}`
|
|
1377
|
-
);
|
|
1378
|
-
} else {
|
|
1379
|
-
updateChainContext(chainKey, `${agentName} 已宿æ§è¡ïŒæªæ£æµå°æä»¶åæŽã`);
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
1246
|
// ââ Capture AI work summary as supplementary chain context ââ
|
|
1247
|
+
// We intentionally do NOT create chain contexts from _workflowFileChanges here.
|
|
1248
|
+
// File-change-based chain contexts ("计åå¶å®æèŠ"/"ææ¡£æŽæ°æèŠ") were removed in v0.6.0
|
|
1249
|
+
// because their M/A/D counts provided negligible value for downstream agents compared
|
|
1250
|
+
// to the agent's own free-text work summary. The file-change approach also created
|
|
1251
|
+
// tight coupling between executeSingleStep and the _workflowFileChanges data structure.
|
|
1383
1252
|
const workSummary = extractFinalOutput(result.output);
|
|
1384
1253
|
if (workSummary) {
|
|
1385
|
-
updateChainContext(`${agentName} å·¥äœæ»ç»`,
|
|
1386
|
-
|
|
1387
|
-
|
|
1254
|
+
updateChainContext(`${agentName} å·¥äœæ»ç»`, `${agentName} 已宿工äœïŒä»¥äžæ¯å
¶å·¥äœæ»ç»ïŒ\n\n${workSummary}`);
|
|
1255
|
+
} else if (result.output) {
|
|
1256
|
+
// ä¿åºïŒäœ¿çšèŸåºå 200 å笊äœäžºç®ç¥æèŠ
|
|
1257
|
+
const fallback = result.output.slice(0, 200).trim();
|
|
1258
|
+
if (fallback) updateChainContext(`${agentName} å·¥äœæ»ç»`, fallback);
|
|
1388
1259
|
}
|
|
1389
1260
|
}
|
|
1390
1261
|
|
|
@@ -1448,37 +1319,6 @@ async function executeLoopGroup(
|
|
|
1448
1319
|
}
|
|
1449
1320
|
}
|
|
1450
1321
|
|
|
1451
|
-
// ââ After loop agent completes: capture changes + stats for chain context ââ
|
|
1452
|
-
const loopAgentChanges = _workflowFileChanges
|
|
1453
|
-
.filter(c => c.stepIndex === stepIndex && c.agent === step.loopAgentName)
|
|
1454
|
-
.map(c => `${c.type === "new" ? "A" : c.type === "delete" ? "D" : "M"} ${c.filePath}`);
|
|
1455
|
-
|
|
1456
|
-
// Use agent-specific keys so worker and trimmer changes coexist without overwriting
|
|
1457
|
-
const chainKey = step.loopAgentName === "worker" ? "代ç 宿œæèŠ" : "代ç ç²Ÿç®æèŠ";
|
|
1458
|
-
|
|
1459
|
-
if (loopAgentChanges.length > 0) {
|
|
1460
|
-
const editCount = loopAgentChanges.filter(c => c.startsWith("M")).length;
|
|
1461
|
-
const newCount = loopAgentChanges.filter(c => c.startsWith("A")).length;
|
|
1462
|
-
const delCount = loopAgentChanges.filter(c => c.startsWith("D")).length;
|
|
1463
|
-
const statsParts: string[] = [];
|
|
1464
|
-
if (editCount > 0) statsParts.push(`ä¿®æ¹ ${editCount} 䞪`);
|
|
1465
|
-
if (newCount > 0) statsParts.push(`æ°å¢ ${newCount} 䞪`);
|
|
1466
|
-
if (delCount > 0) statsParts.push(`å é€ ${delCount} 䞪`);
|
|
1467
|
-
const statsLine = statsParts.length > 0 ? `æ¹åšç»è®¡: ${statsParts.join("ïŒ")}\n\n` : "";
|
|
1468
|
-
|
|
1469
|
-
updateChainContext(chainKey,
|
|
1470
|
-
`${step.loopAgentName === "worker" ? "代ç 宿œ" : "代ç 粟ç®"}已宿ã\n` +
|
|
1471
|
-
statsLine +
|
|
1472
|
-
`åæŽæä»¶å衚:\n${loopAgentChanges.join("\n")}` +
|
|
1473
|
-
(planFileRelPath ? `\n\n宿œè®¡å: ${planFileRelPath}\n(å¯åš .pi-dev-output/pi-plans/ äž grep UUID ${_workflowId} æŸå°)` : "")
|
|
1474
|
-
);
|
|
1475
|
-
} else {
|
|
1476
|
-
updateChainContext(chainKey,
|
|
1477
|
-
`${step.loopAgentName} 已宿æ§è¡ïŒæªæ£æµå°æä»¶åæŽã\n` +
|
|
1478
|
-
(planFileRelPath ? `宿œè®¡å: ${planFileRelPath}\n(å¯åš .pi-dev-output/pi-plans/ äž grep UUID ${_workflowId} æŸå°)` : "")
|
|
1479
|
-
);
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
1322
|
// ââ Capture loop agent work summary as supplementary chain context ââ
|
|
1483
1323
|
const loopFinalText = extractFinalOutput(agentResult.output);
|
|
1484
1324
|
if (loopFinalText) {
|
|
@@ -1525,25 +1365,11 @@ async function executeLoopGroup(
|
|
|
1525
1365
|
}
|
|
1526
1366
|
}
|
|
1527
1367
|
|
|
1528
|
-
// ââ After reviewer: capture review context for next loop ââ
|
|
1529
|
-
// Use agent-specific key so worker-reviewer and trimmer-reviewer feedback don't interfere
|
|
1530
|
-
const reviewChainKey = step.loopAgentName === "worker" ? "代ç 审æ¥åéŠ" : "粟ç®å®¡æ¥åéŠ";
|
|
1531
|
-
if (reviewSummary) {
|
|
1532
|
-
const reviewCountParts: string[] = [];
|
|
1533
|
-
if (reviewSummary.critical > 0) reviewCountParts.push(`䞥é ${reviewSummary.critical} 䞪`);
|
|
1534
|
-
if (reviewSummary.medium > 0) reviewCountParts.push(`äžç ${reviewSummary.medium} 䞪`);
|
|
1535
|
-
if (reviewSummary.low > 0) reviewCountParts.push(`äœ ${reviewSummary.low} 䞪`);
|
|
1536
|
-
const reviewStats = reviewCountParts.length > 0 ? `åç° ${reviewCountParts.join("ïŒ")} é®é¢ã` : "æªåç°é®é¢ã";
|
|
1537
|
-
|
|
1538
|
-
updateChainContext(reviewChainKey,
|
|
1539
|
-
`${reviewStats}\n` +
|
|
1540
|
-
`宿Žå®¡æ¥æ¥ååš .pi-dev-output/pi-review/md/ ç®åœäžïŒ\n` +
|
|
1541
|
-
`请åšå·¥äœæµèŸåºç®åœäž grep UUID "${_workflowId}" æ¥æŸææ°æ¥åã`
|
|
1542
|
-
);
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
1368
|
// ââ Capture reviewer work summary as supplementary chain context ââ
|
|
1546
|
-
//
|
|
1369
|
+
// The old review-chain-context entries ("代ç 审æ¥åéŠ"/"粟ç®å®¡æ¥åéŠ") based on
|
|
1370
|
+
// _workflowFileChanges were removed in v0.6.0 because they duplicated information
|
|
1371
|
+
// from the review report file and polluted chain context with file-path-heavy data.
|
|
1372
|
+
// Reuse already-parsed extractedOutput to avoid double parsing.
|
|
1547
1373
|
if (extractedOutput) {
|
|
1548
1374
|
updateChainContext(`审æ¥å·¥äœæ»ç»`,
|
|
1549
1375
|
`审æ¥è
已宿审æ¥ïŒä»¥äžæ¯å
¶å®¡æ¥æ»ç»ïŒ\n\n${extractedOutput}`
|