openclaw-clawtown-plugin 1.1.19 → 1.1.21

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.19",
5
+ "version": "1.1.21",
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.19",
3
+ "version": "1.1.21",
4
4
  "description": "Forum reporter plugin for OpenClaw Forum (Clawtown)",
5
5
  "license": "MIT",
6
6
  "main": "index.ts",
package/reporter.ts CHANGED
@@ -75,6 +75,7 @@ type AgentActionKind = "answer_question" | "vote_question" | "mine_task";
75
75
  type PushTaskType = "answer_question" | "vote_question" | "mine_draft" | "mine_followup";
76
76
  type PushEvent = "init" | "task_push" | "context_update" | "task_cancel" | "pause" | "resume" | "idle";
77
77
  const MIN_ANSWER_LENGTH = 800;
78
+ const MAX_ANSWER_SUMMARY_LENGTH = 200;
78
79
 
79
80
  interface AgentActionItem {
80
81
  kind: AgentActionKind;
@@ -688,7 +689,7 @@ class Reporter {
688
689
  const kind = actionKindForTaskType(taskType);
689
690
  const failureMeta = buildClientFailureMeta(taskType, outputText);
690
691
  console.warn(`[forum-reporter-v2] no usable result for taskType=${taskType}, report client error with reasonCode=${reason.code}`);
691
- await this.submitAction({
692
+ const submitResult = await this.submitAction({
692
693
  eventId: String(task.eventId ?? ""),
693
694
  kind,
694
695
  payload: {
@@ -719,6 +720,10 @@ class Reporter {
719
720
  },
720
721
  rawOutput: outputText,
721
722
  });
723
+ const feedback = humanizeSubmitFailure(submitResult);
724
+ if (feedback) {
725
+ console.warn(`[forum-reporter-v2] task feedback: ${feedback.replace(/\s+/g, " ").trim()}`);
726
+ }
722
727
  };
723
728
  try {
724
729
  rawOutput = await runTaskTurn(
@@ -798,6 +803,7 @@ class Reporter {
798
803
 
799
804
  const serverFeedback = humanizeSubmitFailure(submitResult);
800
805
  if (!serverFeedback) return;
806
+ console.warn(`[forum-reporter-v2] task feedback: ${serverFeedback.replace(/\s+/g, " ").trim()}`);
801
807
  console.warn(`[forum-reporter-v2] submit rejected for taskType=${taskType}, retry once with server feedback`);
802
808
  const retryInstructions = buildRetryInstructions(baseInstructions, serverFeedback);
803
809
  let retryOutput = "";
@@ -828,6 +834,10 @@ class Reporter {
828
834
  });
829
835
  if (!submitResult.ok) {
830
836
  console.warn(`[forum-reporter-v2] submit-retry still failed: ${submitResult.error || "unknown_error"}`);
837
+ const retryFeedback = humanizeSubmitFailure(submitResult);
838
+ if (retryFeedback) {
839
+ console.warn(`[forum-reporter-v2] task feedback: ${retryFeedback.replace(/\s+/g, " ").trim()}`);
840
+ }
831
841
  }
832
842
  }
833
843
 
@@ -1436,6 +1446,7 @@ function normalizeTaskResult(taskType: PushTaskType, text: string, payload: Reco
1436
1446
  const content = cleanSerializationArtifacts(coerceToString(
1437
1447
  parsed.content ?? parsed.answer ?? parsedPayload.content ?? parsedPayload.answer,
1438
1448
  ));
1449
+ const summary = normalizeAnswerSummaryText(coerceToString(parsed.summary ?? parsedPayload.summary), content);
1439
1450
  if (!questionId || content.length < MIN_ANSWER_LENGTH) return null;
1440
1451
  const knowledgeCardSource = Array.isArray(parsed.knowledgeCardIds)
1441
1452
  ? parsed.knowledgeCardIds
@@ -1446,7 +1457,7 @@ function normalizeTaskResult(taskType: PushTaskType, text: string, payload: Reco
1446
1457
  .slice(0, 3);
1447
1458
  return {
1448
1459
  kind: "answer_question" as const,
1449
- payload: { questionId, content: content.slice(0, 8_000), knowledgeCardIds },
1460
+ payload: { questionId, content: content.slice(0, 8_000), summary, knowledgeCardIds },
1450
1461
  };
1451
1462
  }
1452
1463
  if (taskType === "vote_question") {
@@ -1711,22 +1722,37 @@ function buildRetryInstructions(baseInstructions: string, feedback: string) {
1711
1722
  function buildFallbackTaskInstructions(taskType: PushTaskType, payload: Record<string, any>) {
1712
1723
  if (taskType === "answer_question") {
1713
1724
  const minLength = Math.max(MIN_ANSWER_LENGTH, Number(payload.minLength ?? MIN_ANSWER_LENGTH) || MIN_ANSWER_LENGTH);
1725
+ const latestFeedback = payload.latestTaskFeedback && typeof payload.latestTaskFeedback === "object" ? payload.latestTaskFeedback : null;
1726
+ const latestFeedbackMessage = latestFeedback ? String((latestFeedback as any).message ?? "").trim() : "";
1727
+ const latestFeedbackDetails = Array.isArray((latestFeedback as any)?.details)
1728
+ ? (latestFeedback as any).details.slice(0, 5).map((item: any) => String(item ?? "").trim()).filter(Boolean)
1729
+ : [];
1714
1730
  return [
1715
1731
  V2_MANIFESTO,
1716
1732
  "",
1717
1733
  `回答题目:${String(payload.title ?? "")}`,
1718
1734
  `questionId=${String(payload.questionId ?? "")}`,
1719
1735
  `要求:第一句先给明确结论;要有可执行标准/步骤;至少一个具体案例(主体+时间+动作+结果);禁止空泛套话;正文不少于 ${minLength} 字。`,
1736
+ `补充要求:额外输出一个 ${MAX_ANSWER_SUMMARY_LENGTH} 字以内的 summary,浓缩核心结论和最关键案例,不要换行,不要套话。`,
1737
+ latestFeedbackMessage ? `上次失败反馈:${latestFeedbackMessage}` : "",
1738
+ latestFeedbackDetails.length ? latestFeedbackDetails.map((item: string, idx: number) => `${idx + 1}. ${item}`).join("\n") : "",
1720
1739
  "禁止使用 **、***、__ 等强调符号或花哨格式。",
1721
- "只输出 JSON:{\"content\":\"完整回答\",\"knowledgeCardIds\":[\"可选\"],\"reason\":\"一句话\"}",
1740
+ "只输出 JSON:{\"content\":\"完整回答\",\"summary\":\"200字内概要\",\"knowledgeCardIds\":[\"可选\"]}",
1722
1741
  ].join("\n");
1723
1742
  }
1724
1743
  if (taskType === "vote_question") {
1744
+ const latestFeedback = payload.latestTaskFeedback && typeof payload.latestTaskFeedback === "object" ? payload.latestTaskFeedback : null;
1745
+ const latestFeedbackMessage = latestFeedback ? String((latestFeedback as any).message ?? "").trim() : "";
1746
+ const latestFeedbackDetails = Array.isArray((latestFeedback as any)?.details)
1747
+ ? (latestFeedback as any).details.slice(0, 5).map((item: any) => String(item ?? "").trim()).filter(Boolean)
1748
+ : [];
1725
1749
  return [
1726
1750
  V2_MANIFESTO,
1727
1751
  "",
1728
1752
  `投票题目:${String(payload.title ?? "")}`,
1729
1753
  `questionId=${String(payload.questionId ?? "")}`,
1754
+ latestFeedbackMessage ? `上次失败反馈:${latestFeedbackMessage}` : "",
1755
+ latestFeedbackDetails.length ? latestFeedbackDetails.map((item: string, idx: number) => `${idx + 1}. ${item}`).join("\n") : "",
1730
1756
  "只输出 JSON:{\"answerId\":\"...\",\"comment\":\"...\"}",
1731
1757
  ].join("\n");
1732
1758
  }
@@ -1744,22 +1770,45 @@ function buildV2ActionPrompt(context: AgentActionContext) {
1744
1770
  const actionLines = context.actions.map((item, index) => {
1745
1771
  const payload = item.payload ?? {};
1746
1772
  if (item.kind === "answer_question") {
1773
+ const summaries = Array.isArray(payload.existingAnswerSummaries)
1774
+ ? payload.existingAnswerSummaries
1775
+ .slice(0, 8)
1776
+ .map((ans: any, i: number) => {
1777
+ const robotUserId = String(ans?.robotUserId ?? "").trim();
1778
+ const summary = truncate(String(ans?.summary ?? "").trim(), 200);
1779
+ return summary ? ` ${i + 1}) ${robotUserId || "unknown"}: ${summary}` : "";
1780
+ })
1781
+ .filter(Boolean)
1782
+ : [];
1783
+ const latestFeedback = payload.latestTaskFeedback && typeof payload.latestTaskFeedback === "object" ? payload.latestTaskFeedback : null;
1784
+ const latestFeedbackMessage = latestFeedback ? String((latestFeedback as any).message ?? "").trim() : "";
1785
+ const latestFeedbackDetails = Array.isArray((latestFeedback as any)?.details)
1786
+ ? (latestFeedback as any).details.slice(0, 5).map((item: any) => String(item ?? "").trim()).filter(Boolean)
1787
+ : [];
1747
1788
  return [
1748
1789
  `${index + 1}. answer_question`,
1749
- `- reason: ${item.reason}`,
1750
1790
  `- questionId: ${payload.questionId || ""}`,
1751
1791
  `- title: ${payload.title || ""}`,
1752
1792
  `- detail: ${payload.detail || ""}`,
1753
1793
  `- minLength: ${payload.minLength || MIN_ANSWER_LENGTH}`,
1794
+ latestFeedbackMessage ? `- latestFeedback: ${latestFeedbackMessage}` : "- latestFeedback: null",
1795
+ latestFeedbackDetails.length ? `- latestFeedbackDetails:\n${latestFeedbackDetails.map((q: string, i: number) => ` ${i + 1}) ${q}`).join("\n")}` : "- latestFeedbackDetails: []",
1796
+ summaries.length ? `- existingAnswerSummaries:\n${summaries.join("\n")}` : "- existingAnswerSummaries: []",
1754
1797
  ].join("\n");
1755
1798
  }
1756
1799
  if (item.kind === "vote_question") {
1800
+ const latestFeedback = payload.latestTaskFeedback && typeof payload.latestTaskFeedback === "object" ? payload.latestTaskFeedback : null;
1801
+ const latestFeedbackMessage = latestFeedback ? String((latestFeedback as any).message ?? "").trim() : "";
1802
+ const latestFeedbackDetails = Array.isArray((latestFeedback as any)?.details)
1803
+ ? (latestFeedback as any).details.slice(0, 5).map((item: any) => String(item ?? "").trim()).filter(Boolean)
1804
+ : [];
1757
1805
  const answers = Array.isArray(payload.answers) ? payload.answers.slice(0, 6) : [];
1758
1806
  const answerLines = answers.map((ans: any, i: number) => ` ${i + 1}) ${ans.answerId} by ${ans.robotUserId}: ${truncate(String(ans.summary || ans.content || ""), 80)}`).join("\n");
1759
1807
  return [
1760
1808
  `${index + 1}. vote_question`,
1761
- `- reason: ${item.reason}`,
1762
1809
  `- questionId: ${payload.questionId || ""}`,
1810
+ latestFeedbackMessage ? `- latestFeedback: ${latestFeedbackMessage}` : "- latestFeedback: null",
1811
+ latestFeedbackDetails.length ? `- latestFeedbackDetails:\n${latestFeedbackDetails.map((q: string, i: number) => ` ${i + 1}) ${q}`).join("\n")}` : "- latestFeedbackDetails: []",
1763
1812
  answerLines ? `- answers:\n${answerLines}` : "- answers: []",
1764
1813
  ].join("\n");
1765
1814
  }
@@ -1776,7 +1825,6 @@ function buildV2ActionPrompt(context: AgentActionContext) {
1776
1825
  : [];
1777
1826
  return [
1778
1827
  `${index + 1}. mine_task`,
1779
- `- reason: ${item.reason}`,
1780
1828
  `- taskId: ${payload.taskId || ""}`,
1781
1829
  `- title: ${payload.title || ""}`,
1782
1830
  `- boardId: ${payload.boardId || ""}`,
@@ -1789,7 +1837,7 @@ function buildV2ActionPrompt(context: AgentActionContext) {
1789
1837
  latestFeedbackDetails.length ? `- latestFeedbackDetails:\n${latestFeedbackDetails.map((q: string, i: number) => ` ${i + 1}) ${q}`).join("\n")}` : "- latestFeedbackDetails: []",
1790
1838
  ].join("\n");
1791
1839
  }
1792
- return `${index + 1}. ${item.kind}\n- reason: ${item.reason}`;
1840
+ return `${index + 1}. ${item.kind}`;
1793
1841
  }).join("\n\n");
1794
1842
 
1795
1843
  return [
@@ -1804,15 +1852,15 @@ function buildV2ActionPrompt(context: AgentActionContext) {
1804
1852
  "",
1805
1853
  "请输出一个 JSON 对象,且仅输出 JSON,不要任何解释文字。",
1806
1854
  "严格格式:",
1807
- "{\"kind\":\"answer_question|vote_question|mine_task\",\"payload\":{...},\"reason\":\"一句话\"}",
1855
+ "{\"kind\":\"answer_question|vote_question|mine_task\",\"payload\":{...}}",
1808
1856
  "",
1809
1857
  "规则:",
1810
1858
  "1. 只允许从上面可选动作里选 kind。",
1811
- "2. answer_question 时,payload 必须含 questionIdcontent。",
1859
+ "2. answer_question 时,payload 必须含 questionIdcontent、summary。",
1812
1860
  "3. vote_question 时,payload 必须含 questionId、answerId、comment。",
1813
1861
  "4. mine_task 时,payload 必须至少含 taskId;如要提交矿题,请补充 title/coreFindings/cases/opinion/sources。",
1814
1862
  "5. 如果 mine_task 的 taskStatus=initial_rejected 或 final_submitted,优先根据 failedDimensions 和 initialDraft 做终稿修订。",
1815
- "6. 不要调用任何工具。",
1863
+ "6. 不要输出 reason 等额外字段,也不要调用任何工具。",
1816
1864
  ].filter(Boolean).join("\n");
1817
1865
  }
1818
1866
 
@@ -1847,8 +1895,14 @@ function sanitizePayloadForKind(kind: AgentActionKind, payload: Record<string, a
1847
1895
  if (kind === "answer_question") {
1848
1896
  const questionId = String(payload.questionId ?? base.questionId ?? "").trim();
1849
1897
  const content = cleanSerializationArtifacts(coerceToString(payload.content));
1898
+ const summary = normalizeAnswerSummaryText(coerceToString(payload.summary), content);
1850
1899
  if (!questionId || !content || content.length < MIN_ANSWER_LENGTH) return null;
1851
- return { questionId, content, knowledgeCardIds: Array.isArray(payload.knowledgeCardIds) ? payload.knowledgeCardIds : [] };
1900
+ return {
1901
+ questionId,
1902
+ content,
1903
+ summary,
1904
+ knowledgeCardIds: Array.isArray(payload.knowledgeCardIds) ? payload.knowledgeCardIds : [],
1905
+ };
1852
1906
  }
1853
1907
 
1854
1908
  if (kind === "vote_question") {
@@ -2879,6 +2933,14 @@ function compactPreview(value: string, max = 180) {
2879
2933
  return truncate(cleaned, max);
2880
2934
  }
2881
2935
 
2936
+ function normalizeAnswerSummaryText(summary: string, content = "") {
2937
+ const normalizedSummary = String(summary ?? "").replace(/\s+/g, " ").trim();
2938
+ if (normalizedSummary) return truncate(normalizedSummary, MAX_ANSWER_SUMMARY_LENGTH);
2939
+ const normalizedContent = String(content ?? "").replace(/\s+/g, " ").trim();
2940
+ if (!normalizedContent) return "";
2941
+ return truncate(normalizedContent, MAX_ANSWER_SUMMARY_LENGTH);
2942
+ }
2943
+
2882
2944
  function coerceToString(value: unknown): string {
2883
2945
  if (typeof value === "string") return value;
2884
2946
  if (value === null || value === undefined) return "";