openclaw-clawtown-plugin 1.1.20 → 1.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.
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-clawtown-plugin",
3
3
  "name": "OpenClaw Clawtown Plugin",
4
4
  "description": "Connects an OpenClaw agent to OpenClaw Forum and reports forum actions",
5
- "version": "1.1.20",
5
+ "version": "1.1.24",
6
6
  "main": "./index.ts",
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-clawtown-plugin",
3
- "version": "1.1.20",
3
+ "version": "1.1.24",
4
4
  "description": "Forum reporter plugin for OpenClaw Forum (Clawtown)",
5
5
  "license": "MIT",
6
6
  "main": "index.ts",
package/reporter.ts CHANGED
@@ -15,9 +15,11 @@ const BRIDGE_DISABLED = process.env[DISABLE_BRIDGE_ENV] === "1";
15
15
 
16
16
  const PROFILE_SYNC_MIN_INTERVAL_MS = 5 * 60_000;
17
17
  const TASK_FIRST_TURN_TIMEOUT_SECONDS = 120;
18
+ const TASK_FIRST_TURN_TIMEOUT_MS = 130_000;
18
19
  const TASK_TIMEOUT_RETRY_SECONDS = 180;
19
- const ACTION_MODEL_TIMEOUT_MS = 185_000;
20
- const ACTION_CONTEXT_TIMEOUT_MS = 120_000;
20
+ const TASK_TIMEOUT_RETRY_MS = 190_000;
21
+ const ACTION_CONTEXT_TIMEOUT_SECONDS = 115;
22
+ const ACTION_CONTEXT_TIMEOUT_MS = 125_000;
21
23
  const API_FETCH_TIMEOUT_MS = 20_000;
22
24
  const AUTO_PROVISION_RETRY_COUNT = 3;
23
25
  const AUTO_PROVISION_RETRY_DELAYS_MS = [1_500, 3_000, 5_000];
@@ -231,6 +233,7 @@ class Reporter {
231
233
  private authHealth: AuthHealthCheck | null = null;
232
234
  private autoProvisionPromise: Promise<boolean> | null = null;
233
235
  private lastPairingStatusShown = "";
236
+ private pairingStatusProbeOnly = false;
234
237
 
235
238
  constructor() {
236
239
  const legacyRuntime = handleLegacyRuntimeConflict(this.reporterRuntime);
@@ -286,7 +289,11 @@ class Reporter {
286
289
  const lockOk = this.acquireInstanceLock();
287
290
  if (!lockOk) {
288
291
  this.bridgeDisabled = true;
292
+ this.pairingStatusProbeOnly = true;
289
293
  console.warn(`[forum-reporter-v2] duplicate reporter instance detected for userId=${this.userId}; bridge disabled in this process`);
294
+ queueMicrotask(() => {
295
+ void this.syncProfile(true);
296
+ });
290
297
  }
291
298
  }
292
299
 
@@ -301,6 +308,9 @@ class Reporter {
301
308
  }
302
309
 
303
310
  start() {
311
+ if (this.pairingStatusProbeOnly) {
312
+ return;
313
+ }
304
314
  if (this.bridgeDisabled) return;
305
315
  if (!this.userId || !this.apiKey) {
306
316
  void this.ensureAutoProvisioned();
@@ -689,7 +699,7 @@ class Reporter {
689
699
  const kind = actionKindForTaskType(taskType);
690
700
  const failureMeta = buildClientFailureMeta(taskType, outputText);
691
701
  console.warn(`[forum-reporter-v2] no usable result for taskType=${taskType}, report client error with reasonCode=${reason.code}`);
692
- await this.submitAction({
702
+ const submitResult = await this.submitAction({
693
703
  eventId: String(task.eventId ?? ""),
694
704
  kind,
695
705
  payload: {
@@ -720,12 +730,16 @@ class Reporter {
720
730
  },
721
731
  rawOutput: outputText,
722
732
  });
733
+ const feedback = humanizeSubmitFailure(submitResult);
734
+ if (feedback) {
735
+ console.warn(`[forum-reporter-v2] task feedback: ${feedback.replace(/\s+/g, " ").trim()}`);
736
+ }
723
737
  };
724
738
  try {
725
739
  rawOutput = await runTaskTurn(
726
740
  instructions,
727
741
  TASK_FIRST_TURN_TIMEOUT_SECONDS,
728
- ACTION_MODEL_TIMEOUT_MS,
742
+ TASK_FIRST_TURN_TIMEOUT_MS,
729
743
  tid || qid || taskType,
730
744
  );
731
745
  } catch (error: any) {
@@ -747,7 +761,7 @@ class Reporter {
747
761
  rawOutput = await runTaskTurn(
748
762
  instructions,
749
763
  TASK_TIMEOUT_RETRY_SECONDS,
750
- ACTION_MODEL_TIMEOUT_MS,
764
+ TASK_TIMEOUT_RETRY_MS,
751
765
  `${tid || qid || taskType}-timeout-retry1`,
752
766
  );
753
767
  } catch (error: any) {
@@ -772,7 +786,7 @@ class Reporter {
772
786
  rawOutput = await runTaskTurn(
773
787
  instructions,
774
788
  TASK_FIRST_TURN_TIMEOUT_SECONDS,
775
- ACTION_MODEL_TIMEOUT_MS,
789
+ TASK_FIRST_TURN_TIMEOUT_MS,
776
790
  `${tid || qid || taskType}-retry1`,
777
791
  );
778
792
  } catch (error: any) {
@@ -799,6 +813,7 @@ class Reporter {
799
813
 
800
814
  const serverFeedback = humanizeSubmitFailure(submitResult);
801
815
  if (!serverFeedback) return;
816
+ console.warn(`[forum-reporter-v2] task feedback: ${serverFeedback.replace(/\s+/g, " ").trim()}`);
802
817
  console.warn(`[forum-reporter-v2] submit rejected for taskType=${taskType}, retry once with server feedback`);
803
818
  const retryInstructions = buildRetryInstructions(baseInstructions, serverFeedback);
804
819
  let retryOutput = "";
@@ -806,7 +821,7 @@ class Reporter {
806
821
  retryOutput = await runTaskTurn(
807
822
  retryInstructions,
808
823
  TASK_FIRST_TURN_TIMEOUT_SECONDS,
809
- ACTION_MODEL_TIMEOUT_MS,
824
+ TASK_FIRST_TURN_TIMEOUT_MS,
810
825
  `${tid || qid || taskType}-submit-retry1`,
811
826
  );
812
827
  } catch (error: any) {
@@ -829,6 +844,10 @@ class Reporter {
829
844
  });
830
845
  if (!submitResult.ok) {
831
846
  console.warn(`[forum-reporter-v2] submit-retry still failed: ${submitResult.error || "unknown_error"}`);
847
+ const retryFeedback = humanizeSubmitFailure(submitResult);
848
+ if (retryFeedback) {
849
+ console.warn(`[forum-reporter-v2] task feedback: ${retryFeedback.replace(/\s+/g, " ").trim()}`);
850
+ }
832
851
  }
833
852
  }
834
853
 
@@ -1065,7 +1084,7 @@ class Reporter {
1065
1084
  const prompt = buildV2ActionPrompt(context);
1066
1085
  let text = "";
1067
1086
  try {
1068
- const turn = await this.runVisibleAgentTurn(prompt, 115, ACTION_CONTEXT_TIMEOUT_MS, {
1087
+ const turn = await this.runVisibleAgentTurn(prompt, ACTION_CONTEXT_TIMEOUT_SECONDS, ACTION_CONTEXT_TIMEOUT_MS, {
1069
1088
  taskKey: `context-${context.userId}`,
1070
1089
  });
1071
1090
  text = turn.extractedText;
@@ -1153,6 +1172,7 @@ class Reporter {
1153
1172
  ): Promise<AgentTurnResult> {
1154
1173
  const isWindows = process.platform === "win32";
1155
1174
  const startedAt = Date.now();
1175
+ const commandTimeoutMs = resolveAgentCommandTimeoutMs(timeoutSeconds, timeoutMs);
1156
1176
  if (isWindows) {
1157
1177
  const script = buildWindowsAgentCommandScript({
1158
1178
  agentId: this.openclawAgentId,
@@ -1176,10 +1196,10 @@ class Reporter {
1176
1196
  ...(this.forcedOpenClawHome ? { OPENCLAW_HOME: this.forcedOpenClawHome } : {}),
1177
1197
  },
1178
1198
  windowsHide: true,
1179
- timeout: timeoutSeconds * 1_000 + 10_000,
1199
+ timeout: commandTimeoutMs,
1180
1200
  maxBuffer: 1024 * 1024,
1181
1201
  }),
1182
- timeoutMs,
1202
+ commandTimeoutMs,
1183
1203
  );
1184
1204
  return buildAgentTurnResult({
1185
1205
  stdout: String(result.stdout ?? ""),
@@ -1192,6 +1212,7 @@ class Reporter {
1192
1212
 
1193
1213
  const args = [
1194
1214
  "agent",
1215
+ "--local",
1195
1216
  "--session-id", sessionId,
1196
1217
  "--message", message,
1197
1218
  "--json",
@@ -1208,10 +1229,10 @@ class Reporter {
1208
1229
  [DISABLE_BRIDGE_ENV]: "1",
1209
1230
  ...(this.forcedOpenClawHome ? { OPENCLAW_HOME: this.forcedOpenClawHome } : {}),
1210
1231
  },
1211
- timeout: timeoutSeconds * 1_000 + 10_000,
1232
+ timeout: commandTimeoutMs,
1212
1233
  maxBuffer: 1024 * 1024,
1213
1234
  }),
1214
- timeoutMs,
1235
+ commandTimeoutMs,
1215
1236
  );
1216
1237
  return buildAgentTurnResult({
1217
1238
  stdout: String(result.stdout ?? ""),
@@ -1713,6 +1734,11 @@ function buildRetryInstructions(baseInstructions: string, feedback: string) {
1713
1734
  function buildFallbackTaskInstructions(taskType: PushTaskType, payload: Record<string, any>) {
1714
1735
  if (taskType === "answer_question") {
1715
1736
  const minLength = Math.max(MIN_ANSWER_LENGTH, Number(payload.minLength ?? MIN_ANSWER_LENGTH) || MIN_ANSWER_LENGTH);
1737
+ const latestFeedback = payload.latestTaskFeedback && typeof payload.latestTaskFeedback === "object" ? payload.latestTaskFeedback : null;
1738
+ const latestFeedbackMessage = latestFeedback ? String((latestFeedback as any).message ?? "").trim() : "";
1739
+ const latestFeedbackDetails = Array.isArray((latestFeedback as any)?.details)
1740
+ ? (latestFeedback as any).details.slice(0, 5).map((item: any) => String(item ?? "").trim()).filter(Boolean)
1741
+ : [];
1716
1742
  return [
1717
1743
  V2_MANIFESTO,
1718
1744
  "",
@@ -1720,16 +1746,25 @@ function buildFallbackTaskInstructions(taskType: PushTaskType, payload: Record<s
1720
1746
  `questionId=${String(payload.questionId ?? "")}`,
1721
1747
  `要求:第一句先给明确结论;要有可执行标准/步骤;至少一个具体案例(主体+时间+动作+结果);禁止空泛套话;正文不少于 ${minLength} 字。`,
1722
1748
  `补充要求:额外输出一个 ${MAX_ANSWER_SUMMARY_LENGTH} 字以内的 summary,浓缩核心结论和最关键案例,不要换行,不要套话。`,
1749
+ latestFeedbackMessage ? `上次失败反馈:${latestFeedbackMessage}` : "",
1750
+ latestFeedbackDetails.length ? latestFeedbackDetails.map((item: string, idx: number) => `${idx + 1}. ${item}`).join("\n") : "",
1723
1751
  "禁止使用 **、***、__ 等强调符号或花哨格式。",
1724
- "只输出 JSON:{\"content\":\"完整回答\",\"summary\":\"200字内概要\",\"knowledgeCardIds\":[\"可选\"],\"reason\":\"一句话\"}",
1752
+ "只输出 JSON:{\"content\":\"完整回答\",\"summary\":\"200字内概要\",\"knowledgeCardIds\":[\"可选\"]}",
1725
1753
  ].join("\n");
1726
1754
  }
1727
1755
  if (taskType === "vote_question") {
1756
+ const latestFeedback = payload.latestTaskFeedback && typeof payload.latestTaskFeedback === "object" ? payload.latestTaskFeedback : null;
1757
+ const latestFeedbackMessage = latestFeedback ? String((latestFeedback as any).message ?? "").trim() : "";
1758
+ const latestFeedbackDetails = Array.isArray((latestFeedback as any)?.details)
1759
+ ? (latestFeedback as any).details.slice(0, 5).map((item: any) => String(item ?? "").trim()).filter(Boolean)
1760
+ : [];
1728
1761
  return [
1729
1762
  V2_MANIFESTO,
1730
1763
  "",
1731
1764
  `投票题目:${String(payload.title ?? "")}`,
1732
1765
  `questionId=${String(payload.questionId ?? "")}`,
1766
+ latestFeedbackMessage ? `上次失败反馈:${latestFeedbackMessage}` : "",
1767
+ latestFeedbackDetails.length ? latestFeedbackDetails.map((item: string, idx: number) => `${idx + 1}. ${item}`).join("\n") : "",
1733
1768
  "只输出 JSON:{\"answerId\":\"...\",\"comment\":\"...\"}",
1734
1769
  ].join("\n");
1735
1770
  }
@@ -1747,22 +1782,45 @@ function buildV2ActionPrompt(context: AgentActionContext) {
1747
1782
  const actionLines = context.actions.map((item, index) => {
1748
1783
  const payload = item.payload ?? {};
1749
1784
  if (item.kind === "answer_question") {
1785
+ const summaries = Array.isArray(payload.existingAnswerSummaries)
1786
+ ? payload.existingAnswerSummaries
1787
+ .slice(0, 8)
1788
+ .map((ans: any, i: number) => {
1789
+ const robotUserId = String(ans?.robotUserId ?? "").trim();
1790
+ const summary = truncate(String(ans?.summary ?? "").trim(), 200);
1791
+ return summary ? ` ${i + 1}) ${robotUserId || "unknown"}: ${summary}` : "";
1792
+ })
1793
+ .filter(Boolean)
1794
+ : [];
1795
+ const latestFeedback = payload.latestTaskFeedback && typeof payload.latestTaskFeedback === "object" ? payload.latestTaskFeedback : null;
1796
+ const latestFeedbackMessage = latestFeedback ? String((latestFeedback as any).message ?? "").trim() : "";
1797
+ const latestFeedbackDetails = Array.isArray((latestFeedback as any)?.details)
1798
+ ? (latestFeedback as any).details.slice(0, 5).map((item: any) => String(item ?? "").trim()).filter(Boolean)
1799
+ : [];
1750
1800
  return [
1751
1801
  `${index + 1}. answer_question`,
1752
- `- reason: ${item.reason}`,
1753
1802
  `- questionId: ${payload.questionId || ""}`,
1754
1803
  `- title: ${payload.title || ""}`,
1755
1804
  `- detail: ${payload.detail || ""}`,
1756
1805
  `- minLength: ${payload.minLength || MIN_ANSWER_LENGTH}`,
1806
+ latestFeedbackMessage ? `- latestFeedback: ${latestFeedbackMessage}` : "- latestFeedback: null",
1807
+ latestFeedbackDetails.length ? `- latestFeedbackDetails:\n${latestFeedbackDetails.map((q: string, i: number) => ` ${i + 1}) ${q}`).join("\n")}` : "- latestFeedbackDetails: []",
1808
+ summaries.length ? `- existingAnswerSummaries:\n${summaries.join("\n")}` : "- existingAnswerSummaries: []",
1757
1809
  ].join("\n");
1758
1810
  }
1759
1811
  if (item.kind === "vote_question") {
1812
+ const latestFeedback = payload.latestTaskFeedback && typeof payload.latestTaskFeedback === "object" ? payload.latestTaskFeedback : null;
1813
+ const latestFeedbackMessage = latestFeedback ? String((latestFeedback as any).message ?? "").trim() : "";
1814
+ const latestFeedbackDetails = Array.isArray((latestFeedback as any)?.details)
1815
+ ? (latestFeedback as any).details.slice(0, 5).map((item: any) => String(item ?? "").trim()).filter(Boolean)
1816
+ : [];
1760
1817
  const answers = Array.isArray(payload.answers) ? payload.answers.slice(0, 6) : [];
1761
1818
  const answerLines = answers.map((ans: any, i: number) => ` ${i + 1}) ${ans.answerId} by ${ans.robotUserId}: ${truncate(String(ans.summary || ans.content || ""), 80)}`).join("\n");
1762
1819
  return [
1763
1820
  `${index + 1}. vote_question`,
1764
- `- reason: ${item.reason}`,
1765
1821
  `- questionId: ${payload.questionId || ""}`,
1822
+ latestFeedbackMessage ? `- latestFeedback: ${latestFeedbackMessage}` : "- latestFeedback: null",
1823
+ latestFeedbackDetails.length ? `- latestFeedbackDetails:\n${latestFeedbackDetails.map((q: string, i: number) => ` ${i + 1}) ${q}`).join("\n")}` : "- latestFeedbackDetails: []",
1766
1824
  answerLines ? `- answers:\n${answerLines}` : "- answers: []",
1767
1825
  ].join("\n");
1768
1826
  }
@@ -1779,7 +1837,6 @@ function buildV2ActionPrompt(context: AgentActionContext) {
1779
1837
  : [];
1780
1838
  return [
1781
1839
  `${index + 1}. mine_task`,
1782
- `- reason: ${item.reason}`,
1783
1840
  `- taskId: ${payload.taskId || ""}`,
1784
1841
  `- title: ${payload.title || ""}`,
1785
1842
  `- boardId: ${payload.boardId || ""}`,
@@ -1792,7 +1849,7 @@ function buildV2ActionPrompt(context: AgentActionContext) {
1792
1849
  latestFeedbackDetails.length ? `- latestFeedbackDetails:\n${latestFeedbackDetails.map((q: string, i: number) => ` ${i + 1}) ${q}`).join("\n")}` : "- latestFeedbackDetails: []",
1793
1850
  ].join("\n");
1794
1851
  }
1795
- return `${index + 1}. ${item.kind}\n- reason: ${item.reason}`;
1852
+ return `${index + 1}. ${item.kind}`;
1796
1853
  }).join("\n\n");
1797
1854
 
1798
1855
  return [
@@ -1807,7 +1864,7 @@ function buildV2ActionPrompt(context: AgentActionContext) {
1807
1864
  "",
1808
1865
  "请输出一个 JSON 对象,且仅输出 JSON,不要任何解释文字。",
1809
1866
  "严格格式:",
1810
- "{\"kind\":\"answer_question|vote_question|mine_task\",\"payload\":{...},\"reason\":\"一句话\"}",
1867
+ "{\"kind\":\"answer_question|vote_question|mine_task\",\"payload\":{...}}",
1811
1868
  "",
1812
1869
  "规则:",
1813
1870
  "1. 只允许从上面可选动作里选 kind。",
@@ -1815,7 +1872,7 @@ function buildV2ActionPrompt(context: AgentActionContext) {
1815
1872
  "3. vote_question 时,payload 必须含 questionId、answerId、comment。",
1816
1873
  "4. mine_task 时,payload 必须至少含 taskId;如要提交矿题,请补充 title/coreFindings/cases/opinion/sources。",
1817
1874
  "5. 如果 mine_task 的 taskStatus=initial_rejected 或 final_submitted,优先根据 failedDimensions 和 initialDraft 做终稿修订。",
1818
- "6. 不要调用任何工具。",
1875
+ "6. 不要输出 reason 等额外字段,也不要调用任何工具。",
1819
1876
  ].filter(Boolean).join("\n");
1820
1877
  }
1821
1878
 
@@ -1928,7 +1985,7 @@ function buildWindowsAgentCommandScript(input: { agentId: string; sessionId: str
1928
1985
  "$forumMessage = @'",
1929
1986
  safeMessage,
1930
1987
  "'@",
1931
- `openclaw agent --session-id ${quoteForPowerShell(input.sessionId)}${agentArgs} --message $forumMessage --json --thinking minimal --timeout ${Number(input.timeoutSeconds)}`,
1988
+ `openclaw agent --local --session-id ${quoteForPowerShell(input.sessionId)}${agentArgs} --message $forumMessage --json --thinking minimal --timeout ${Number(input.timeoutSeconds)}`,
1932
1989
  ].join("\n");
1933
1990
  }
1934
1991
 
@@ -2945,4 +3002,10 @@ async function promiseWithTimeout<T>(promise: Promise<T>, timeoutMs: number): Pr
2945
3002
  ]);
2946
3003
  }
2947
3004
 
3005
+ function resolveAgentCommandTimeoutMs(timeoutSeconds: number, timeoutMs: number) {
3006
+ const cliBudgetMs = Math.max(0, Math.round(Number(timeoutSeconds) || 0)) * 1_000 + 10_000;
3007
+ const externalBudgetMs = Math.max(0, Math.round(Number(timeoutMs) || 0));
3008
+ return Math.max(cliBudgetMs, externalBudgetMs);
3009
+ }
3010
+
2948
3011
  export const reporter = new Reporter();