scientify 1.13.6 → 2.0.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/README.en.md +350 -0
- package/README.md +148 -358
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +131 -122
- package/dist/index.js.map +1 -1
- package/dist/src/cli/research.d.ts +1 -6
- package/dist/src/cli/research.d.ts.map +1 -1
- package/dist/src/cli/research.js +227 -123
- package/dist/src/cli/research.js.map +1 -1
- package/dist/src/commands/metabolism-status.d.ts +3 -3
- package/dist/src/commands/metabolism-status.d.ts.map +1 -1
- package/dist/src/commands/metabolism-status.js +72 -75
- package/dist/src/commands/metabolism-status.js.map +1 -1
- package/dist/src/commands.d.ts +1 -1
- package/dist/src/commands.d.ts.map +1 -1
- package/dist/src/commands.js +0 -55
- package/dist/src/commands.js.map +1 -1
- package/dist/src/hooks/cron-skill-inject.d.ts +6 -7
- package/dist/src/hooks/cron-skill-inject.d.ts.map +1 -1
- package/dist/src/hooks/cron-skill-inject.js +6 -15
- package/dist/src/hooks/cron-skill-inject.js.map +1 -1
- package/dist/src/hooks/research-mode.d.ts +1 -1
- package/dist/src/hooks/research-mode.d.ts.map +1 -1
- package/dist/src/hooks/research-mode.js +24 -101
- package/dist/src/hooks/research-mode.js.map +1 -1
- package/dist/src/hooks/scientify-signature.d.ts +1 -1
- package/dist/src/hooks/scientify-signature.d.ts.map +1 -1
- package/dist/src/hooks/scientify-signature.js +2 -5
- package/dist/src/hooks/scientify-signature.js.map +1 -1
- package/dist/src/knowledge-state/render.d.ts +1 -9
- package/dist/src/knowledge-state/render.d.ts.map +1 -1
- package/dist/src/knowledge-state/render.js +33 -187
- package/dist/src/knowledge-state/render.js.map +1 -1
- package/dist/src/knowledge-state/store.d.ts.map +1 -1
- package/dist/src/knowledge-state/store.js +65 -1100
- package/dist/src/knowledge-state/store.js.map +1 -1
- package/dist/src/knowledge-state/types.d.ts +0 -76
- package/dist/src/knowledge-state/types.d.ts.map +1 -1
- package/dist/src/literature/subscription-state.d.ts +0 -2
- package/dist/src/literature/subscription-state.d.ts.map +1 -1
- package/dist/src/literature/subscription-state.js +7 -1375
- package/dist/src/literature/subscription-state.js.map +1 -1
- package/dist/src/research-subscriptions/constants.d.ts +1 -1
- package/dist/src/research-subscriptions/constants.js +1 -1
- package/dist/src/research-subscriptions/cron-client.d.ts +1 -1
- package/dist/src/research-subscriptions/cron-client.d.ts.map +1 -1
- package/dist/src/research-subscriptions/delivery.d.ts +1 -1
- package/dist/src/research-subscriptions/delivery.d.ts.map +1 -1
- package/dist/src/research-subscriptions/handlers.d.ts +1 -1
- package/dist/src/research-subscriptions/handlers.d.ts.map +1 -1
- package/dist/src/research-subscriptions/handlers.js +10 -20
- package/dist/src/research-subscriptions/handlers.js.map +1 -1
- package/dist/src/research-subscriptions/parse.d.ts.map +1 -1
- package/dist/src/research-subscriptions/parse.js +0 -25
- package/dist/src/research-subscriptions/parse.js.map +1 -1
- package/dist/src/research-subscriptions/prompt.d.ts +1 -1
- package/dist/src/research-subscriptions/prompt.d.ts.map +1 -1
- package/dist/src/research-subscriptions/prompt.js +195 -244
- package/dist/src/research-subscriptions/prompt.js.map +1 -1
- package/dist/src/research-subscriptions/types.d.ts +1 -3
- package/dist/src/research-subscriptions/types.d.ts.map +1 -1
- package/dist/src/templates/bootstrap.d.ts.map +1 -1
- package/dist/src/templates/bootstrap.js +32 -19
- package/dist/src/templates/bootstrap.js.map +1 -1
- package/dist/src/tools/arxiv-download.d.ts +1 -2
- package/dist/src/tools/arxiv-download.d.ts.map +1 -1
- package/dist/src/tools/arxiv-search.d.ts +1 -2
- package/dist/src/tools/arxiv-search.d.ts.map +1 -1
- package/dist/src/tools/github-search-tool.d.ts +1 -2
- package/dist/src/tools/github-search-tool.d.ts.map +1 -1
- package/dist/src/tools/openalex-search.d.ts +1 -2
- package/dist/src/tools/openalex-search.d.ts.map +1 -1
- package/dist/src/tools/openreview-lookup.d.ts +1 -2
- package/dist/src/tools/openreview-lookup.d.ts.map +1 -1
- package/dist/src/tools/paper-browser.d.ts +1 -2
- package/dist/src/tools/paper-browser.d.ts.map +1 -1
- package/dist/src/tools/result.d.ts +3 -5
- package/dist/src/tools/result.d.ts.map +1 -1
- package/dist/src/tools/result.js +5 -7
- package/dist/src/tools/result.js.map +1 -1
- package/dist/src/tools/scientify-cron.d.ts +4 -11
- package/dist/src/tools/scientify-cron.d.ts.map +1 -1
- package/dist/src/tools/scientify-cron.js +19 -524
- package/dist/src/tools/scientify-cron.js.map +1 -1
- package/dist/src/tools/scientify-literature-state.d.ts +1 -76
- package/dist/src/tools/scientify-literature-state.d.ts.map +1 -1
- package/dist/src/tools/scientify-literature-state.js +46 -363
- package/dist/src/tools/scientify-literature-state.js.map +1 -1
- package/dist/src/tools/unpaywall-download.d.ts +1 -2
- package/dist/src/tools/unpaywall-download.d.ts.map +1 -1
- package/dist/src/types.d.ts +16 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/openclaw.plugin.json +4 -2
- package/package.json +1 -1
- package/skills/metabolism/SKILL.md +2 -0
- package/skills/research-subscription/SKILL.md +1 -29
- package/README.zh.md +0 -494
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
|
-
import {
|
|
3
|
-
import { parseSubscribeOptions } from "../research-subscriptions/parse.js";
|
|
2
|
+
import { normalizeDeliveryChannelOverride } from "../research-subscriptions/delivery.js";
|
|
4
3
|
import { createResearchSubscribeHandler, createResearchSubscriptionsHandler, createResearchUnsubscribeHandler, } from "../research-subscriptions.js";
|
|
5
|
-
import { getIncrementalStateStatus, recordIncrementalPush } from "../literature/subscription-state.js";
|
|
6
4
|
import { Result } from "./result.js";
|
|
7
5
|
export const ScientifyCronToolSchema = Type.Object({
|
|
8
|
-
action: Type.
|
|
9
|
-
description: 'Action: "upsert" | "list" | "remove".
|
|
10
|
-
})
|
|
6
|
+
action: Type.String({
|
|
7
|
+
description: 'Action: "upsert" | "list" | "remove".',
|
|
8
|
+
}),
|
|
11
9
|
scope: Type.Optional(Type.String({
|
|
12
10
|
description: "Scope key for grouping jobs (e.g. user ID or thread ID). Default: global.",
|
|
13
11
|
})),
|
|
@@ -50,15 +48,6 @@ export const ScientifyCronToolSchema = Type.Object({
|
|
|
50
48
|
no_deliver: Type.Optional(Type.Boolean({
|
|
51
49
|
description: "If true, run in background without push delivery.",
|
|
52
50
|
})),
|
|
53
|
-
metadata_only: Type.Optional(Type.Boolean({
|
|
54
|
-
description: "If true, allow metadata-only reading (skip full-text-first strict default). Use only when user explicitly requests it.",
|
|
55
|
-
})),
|
|
56
|
-
language: Type.Optional(Type.String({
|
|
57
|
-
description: 'Optional output language hint: "zh", "en", or "auto".',
|
|
58
|
-
})),
|
|
59
|
-
run_now: Type.Optional(Type.Boolean({
|
|
60
|
-
description: "If true (upsert only), trigger one immediate run after job creation/update; for research tasks, also return a status_json snapshot.",
|
|
61
|
-
})),
|
|
62
51
|
job_id: Type.Optional(Type.String({
|
|
63
52
|
description: "Specific job id to remove (only used when action=remove).",
|
|
64
53
|
})),
|
|
@@ -72,78 +61,6 @@ function readStringParam(params, key) {
|
|
|
72
61
|
const str = String(value).trim();
|
|
73
62
|
return str.length > 0 ? str : undefined;
|
|
74
63
|
}
|
|
75
|
-
function sanitizeProjectId(raw) {
|
|
76
|
-
const trimmed = raw.trim();
|
|
77
|
-
if (/^[A-Za-z0-9_-]+$/.test(trimmed))
|
|
78
|
-
return trimmed;
|
|
79
|
-
const slug = trimmed
|
|
80
|
-
.toLowerCase()
|
|
81
|
-
.replace(/[^a-z0-9_-]+/g, "-")
|
|
82
|
-
.replace(/-+/g, "-")
|
|
83
|
-
.replace(/^-|-$/g, "");
|
|
84
|
-
return slug || "project";
|
|
85
|
-
}
|
|
86
|
-
function normalizeScheduleInput(raw, runNow) {
|
|
87
|
-
if (!raw)
|
|
88
|
-
return undefined;
|
|
89
|
-
const trimmed = raw.trim();
|
|
90
|
-
if (!trimmed)
|
|
91
|
-
return undefined;
|
|
92
|
-
const lower = trimmed.toLowerCase();
|
|
93
|
-
if (["now", "immediate", "immediately", "right now", "asap", "立即", "马上", "立刻"].includes(lower)) {
|
|
94
|
-
// run_now already triggers immediate execution; keep a valid persistent schedule.
|
|
95
|
-
return runNow ? "daily 09:00 Asia/Shanghai" : "at 2m";
|
|
96
|
-
}
|
|
97
|
-
if (/^\d+[smhdw]$/i.test(trimmed)) {
|
|
98
|
-
return `at ${trimmed}`;
|
|
99
|
-
}
|
|
100
|
-
if (/^every\s*hour$/i.test(trimmed) || /^每小时$/u.test(trimmed)) {
|
|
101
|
-
return "every 1h";
|
|
102
|
-
}
|
|
103
|
-
// Guard against `at <past-time>` generated by model/tool callers.
|
|
104
|
-
if (lower.startsWith("at ")) {
|
|
105
|
-
const when = trimmed.slice(3).trim();
|
|
106
|
-
if (when) {
|
|
107
|
-
const atMs = Date.parse(when);
|
|
108
|
-
if (!Number.isNaN(atMs) && atMs <= Date.now()) {
|
|
109
|
-
return runNow ? "daily 09:00 Asia/Shanghai" : "at 2m";
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return trimmed;
|
|
114
|
-
}
|
|
115
|
-
function inferAction(params) {
|
|
116
|
-
const raw = readStringParam(params, "action")?.toLowerCase();
|
|
117
|
-
if (raw) {
|
|
118
|
-
if (["upsert", "create", "add", "set", "update", "start", "schedule", "new", "insert"].includes(raw)) {
|
|
119
|
-
return "upsert";
|
|
120
|
-
}
|
|
121
|
-
if (["list", "show", "ls", "status"].includes(raw)) {
|
|
122
|
-
return "list";
|
|
123
|
-
}
|
|
124
|
-
if (["remove", "delete", "cancel", "rm", "unsubscribe"].includes(raw)) {
|
|
125
|
-
return "remove";
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
const hasJobId = Boolean(readStringParam(params, "job_id"));
|
|
129
|
-
const hasUpsertSignals = Boolean(readStringParam(params, "schedule")) ||
|
|
130
|
-
Boolean(readStringParam(params, "topic")) ||
|
|
131
|
-
Boolean(readStringParam(params, "message")) ||
|
|
132
|
-
Boolean(readStringParam(params, "project")) ||
|
|
133
|
-
readBooleanParam(params, "run_now") ||
|
|
134
|
-
readBooleanParam(params, "no_deliver") ||
|
|
135
|
-
readBooleanParam(params, "metadata_only") ||
|
|
136
|
-
readNumberParam(params, "max_papers") !== undefined ||
|
|
137
|
-
readNumberParam(params, "recency_days") !== undefined ||
|
|
138
|
-
readNumberParam(params, "candidate_pool") !== undefined ||
|
|
139
|
-
Boolean(readStringParam(params, "channel")) ||
|
|
140
|
-
Boolean(readStringParam(params, "to"));
|
|
141
|
-
if (hasUpsertSignals)
|
|
142
|
-
return "upsert";
|
|
143
|
-
if (hasJobId)
|
|
144
|
-
return "remove";
|
|
145
|
-
return "list";
|
|
146
|
-
}
|
|
147
64
|
function readBooleanParam(params, key) {
|
|
148
65
|
return params[key] === true;
|
|
149
66
|
}
|
|
@@ -186,15 +103,6 @@ function normalizeScope(raw) {
|
|
|
186
103
|
.replace(/^-|-$/g, "");
|
|
187
104
|
return base || "global";
|
|
188
105
|
}
|
|
189
|
-
function parseJobIdFromResultText(text) {
|
|
190
|
-
const fenced = text.match(/Job ID:\s*`([^`]+)`/i);
|
|
191
|
-
if (fenced?.[1])
|
|
192
|
-
return fenced[1].trim();
|
|
193
|
-
const plain = text.match(/Job ID:\s*([a-z0-9-]{8,})/i);
|
|
194
|
-
if (plain?.[1])
|
|
195
|
-
return plain[1].trim();
|
|
196
|
-
return undefined;
|
|
197
|
-
}
|
|
198
106
|
function buildToolContext(scope, args, commandBody) {
|
|
199
107
|
return {
|
|
200
108
|
senderId: `tool_${scope}`,
|
|
@@ -203,13 +111,18 @@ function buildToolContext(scope, args, commandBody) {
|
|
|
203
111
|
args,
|
|
204
112
|
commandBody,
|
|
205
113
|
config: {},
|
|
114
|
+
requestConversationBinding: async () => ({ status: "error", message: "not available in tool context" }),
|
|
115
|
+
detachConversationBinding: async () => ({ removed: false }),
|
|
116
|
+
getCurrentConversationBinding: async () => null,
|
|
206
117
|
};
|
|
207
118
|
}
|
|
208
119
|
function getResultText(result) {
|
|
209
|
-
return result.text ??
|
|
120
|
+
return result.text ?? "";
|
|
210
121
|
}
|
|
211
122
|
function getResultError(result) {
|
|
212
|
-
|
|
123
|
+
if (!result.isError)
|
|
124
|
+
return undefined;
|
|
125
|
+
const maybe = result.text?.trim();
|
|
213
126
|
return maybe && maybe.length > 0 ? maybe : undefined;
|
|
214
127
|
}
|
|
215
128
|
function shouldPromoteMessageToTopic(message) {
|
|
@@ -235,272 +148,26 @@ function deriveTopicFromResearchMessage(message) {
|
|
|
235
148
|
const normalized = text.trim();
|
|
236
149
|
return normalized.length > 0 ? normalized : message.trim();
|
|
237
150
|
}
|
|
238
|
-
function
|
|
151
|
+
function buildSubscribeArgs(params) {
|
|
152
|
+
const parts = [];
|
|
153
|
+
const schedule = readStringParam(params, "schedule") ?? "daily 09:00 Asia/Shanghai";
|
|
154
|
+
parts.push(schedule);
|
|
239
155
|
let topic = readStringParam(params, "topic");
|
|
240
156
|
let message = readStringParam(params, "message");
|
|
241
157
|
if (!topic && message && shouldPromoteMessageToTopic(message)) {
|
|
242
158
|
topic = deriveTopicFromResearchMessage(message);
|
|
243
159
|
message = undefined;
|
|
244
160
|
}
|
|
245
|
-
return { topic, message };
|
|
246
|
-
}
|
|
247
|
-
function parseIncrementalScopeFromResultText(text) {
|
|
248
|
-
const fenced = text.match(/Incremental Scope:\s*`([^`]+)`/i);
|
|
249
|
-
if (fenced?.[1])
|
|
250
|
-
return fenced[1].trim();
|
|
251
|
-
const plain = text.match(/Incremental Scope:\s*([^\n]+)/i);
|
|
252
|
-
if (plain?.[1])
|
|
253
|
-
return plain[1].trim();
|
|
254
|
-
return undefined;
|
|
255
|
-
}
|
|
256
|
-
function latestRunId(status) {
|
|
257
|
-
return status?.recentChangeStats[0]?.runId;
|
|
258
|
-
}
|
|
259
|
-
function lastRunAtMs(status) {
|
|
260
|
-
return status?.knowledgeStateSummary?.lastRunAtMs ?? 0;
|
|
261
|
-
}
|
|
262
|
-
function lastPushedAtMs(status) {
|
|
263
|
-
return status?.lastPushedAtMs ?? 0;
|
|
264
|
-
}
|
|
265
|
-
function hasFreshRun(before, after, runStartedAtMs) {
|
|
266
|
-
const beforeRunId = latestRunId(before);
|
|
267
|
-
const afterRunId = latestRunId(after);
|
|
268
|
-
const hasFreshTimestamp = lastRunAtMs(after) >= runStartedAtMs || lastPushedAtMs(after) >= runStartedAtMs;
|
|
269
|
-
if (!before) {
|
|
270
|
-
return hasFreshTimestamp;
|
|
271
|
-
}
|
|
272
|
-
if (after.totalRuns > before.totalRuns)
|
|
273
|
-
return true;
|
|
274
|
-
if (afterRunId && beforeRunId && afterRunId !== beforeRunId)
|
|
275
|
-
return true;
|
|
276
|
-
if (!beforeRunId && afterRunId)
|
|
277
|
-
return true;
|
|
278
|
-
if (lastRunAtMs(after) > lastRunAtMs(before))
|
|
279
|
-
return true;
|
|
280
|
-
if (lastPushedAtMs(after) > lastPushedAtMs(before))
|
|
281
|
-
return true;
|
|
282
|
-
if (hasFreshTimestamp &&
|
|
283
|
-
lastRunAtMs(before) < runStartedAtMs &&
|
|
284
|
-
lastPushedAtMs(before) < runStartedAtMs) {
|
|
285
|
-
return true;
|
|
286
|
-
}
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
function uniqueScopeCandidates(candidates) {
|
|
290
|
-
const seen = new Set();
|
|
291
|
-
const result = [];
|
|
292
|
-
for (const candidate of candidates) {
|
|
293
|
-
if (!candidate)
|
|
294
|
-
continue;
|
|
295
|
-
const normalized = normalizeScope(candidate);
|
|
296
|
-
if (!normalized || seen.has(normalized))
|
|
297
|
-
continue;
|
|
298
|
-
seen.add(normalized);
|
|
299
|
-
result.push(normalized);
|
|
300
|
-
}
|
|
301
|
-
return result;
|
|
302
|
-
}
|
|
303
|
-
function buildFallbackRunId(jobId) {
|
|
304
|
-
const ts = new Date().toISOString().replace(/[-:.]/g, "").replace("T", "t").replace("Z", "z");
|
|
305
|
-
return `cron-${jobId}-${ts}-autofallback`;
|
|
306
|
-
}
|
|
307
|
-
function sleep(ms) {
|
|
308
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
309
|
-
}
|
|
310
|
-
function parseCronRunMarker(raw) {
|
|
311
|
-
const text = (raw ?? "").trim();
|
|
312
|
-
if (!text)
|
|
313
|
-
return undefined;
|
|
314
|
-
try {
|
|
315
|
-
const parsed = JSON.parse(text);
|
|
316
|
-
return {
|
|
317
|
-
ok: typeof parsed.ok === "boolean" ? parsed.ok : undefined,
|
|
318
|
-
ran: typeof parsed.ran === "boolean" ? parsed.ran : undefined,
|
|
319
|
-
reason: typeof parsed.reason === "string" ? parsed.reason : undefined,
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
catch {
|
|
323
|
-
return undefined;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
function serializeRunStatusSnapshot(status) {
|
|
327
|
-
const projectRecentPapers = status.knowledgeStateSummary?.recentPapers ?? [];
|
|
328
|
-
const globalById = new Map(status.recentPapers.map((paper) => [paper.id, paper]));
|
|
329
|
-
return {
|
|
330
|
-
scope: status.scope,
|
|
331
|
-
topic: status.topic,
|
|
332
|
-
topic_key: status.topicKey,
|
|
333
|
-
known_paper_count: status.knownPaperCount,
|
|
334
|
-
total_runs: status.totalRuns,
|
|
335
|
-
last_status: status.lastStatus ?? null,
|
|
336
|
-
last_pushed_at_ms: status.lastPushedAtMs ?? null,
|
|
337
|
-
latest_run_id: status.recentChangeStats[0]?.runId ?? null,
|
|
338
|
-
knowledge_state_summary: status.knowledgeStateSummary
|
|
339
|
-
? {
|
|
340
|
-
project_id: status.knowledgeStateSummary.projectId,
|
|
341
|
-
stream_key: status.knowledgeStateSummary.streamKey,
|
|
342
|
-
run_profile: status.knowledgeStateSummary.runProfile,
|
|
343
|
-
total_runs: status.knowledgeStateSummary.totalRuns,
|
|
344
|
-
total_hypotheses: status.knowledgeStateSummary.totalHypotheses,
|
|
345
|
-
knowledge_topics_count: status.knowledgeStateSummary.knowledgeTopicsCount,
|
|
346
|
-
paper_notes_count: status.knowledgeStateSummary.paperNotesCount,
|
|
347
|
-
trigger_state: {
|
|
348
|
-
consecutive_new_revise_days: status.knowledgeStateSummary.triggerState.consecutiveNewReviseDays,
|
|
349
|
-
bridge_count_7d: status.knowledgeStateSummary.triggerState.bridgeCount7d,
|
|
350
|
-
unread_core_backlog: status.knowledgeStateSummary.triggerState.unreadCoreBacklog,
|
|
351
|
-
last_updated_at_ms: status.knowledgeStateSummary.triggerState.lastUpdatedAtMs,
|
|
352
|
-
},
|
|
353
|
-
quality_gate: {
|
|
354
|
-
mode: status.knowledgeStateSummary.qualityGate.mode,
|
|
355
|
-
severity: status.knowledgeStateSummary.qualityGate.severity,
|
|
356
|
-
warnings: status.knowledgeStateSummary.qualityGate.warnings,
|
|
357
|
-
fatal_reasons: status.knowledgeStateSummary.qualityGate.fatalReasons,
|
|
358
|
-
blocking: status.knowledgeStateSummary.qualityGate.blocking,
|
|
359
|
-
passed: status.knowledgeStateSummary.qualityGate.passed,
|
|
360
|
-
full_text_coverage_pct: status.knowledgeStateSummary.qualityGate.fullTextCoveragePct,
|
|
361
|
-
evidence_binding_rate_pct: status.knowledgeStateSummary.qualityGate.evidenceBindingRatePct,
|
|
362
|
-
citation_error_rate_pct: status.knowledgeStateSummary.qualityGate.citationErrorRatePct,
|
|
363
|
-
reasons: status.knowledgeStateSummary.qualityGate.reasons,
|
|
364
|
-
},
|
|
365
|
-
hypothesis_gate: {
|
|
366
|
-
accepted: status.knowledgeStateSummary.hypothesisGate.accepted,
|
|
367
|
-
rejected: status.knowledgeStateSummary.hypothesisGate.rejected,
|
|
368
|
-
rejection_reasons: status.knowledgeStateSummary.hypothesisGate.rejectionReasons,
|
|
369
|
-
},
|
|
370
|
-
last_run_at_ms: status.knowledgeStateSummary.lastRunAtMs ?? null,
|
|
371
|
-
last_status: status.knowledgeStateSummary.lastStatus ?? null,
|
|
372
|
-
recent_hypotheses: status.knowledgeStateSummary.recentHypotheses.map((item) => ({
|
|
373
|
-
id: item.id,
|
|
374
|
-
statement: item.statement,
|
|
375
|
-
trigger: item.trigger,
|
|
376
|
-
created_at_ms: item.createdAtMs,
|
|
377
|
-
file: item.file,
|
|
378
|
-
strict_overall_score: typeof item.strictOverallScore === "number" ? item.strictOverallScore : null,
|
|
379
|
-
strict_decision: item.strictDecision ?? null,
|
|
380
|
-
})),
|
|
381
|
-
last_reflection_tasks: status.knowledgeStateSummary.lastReflectionTasks,
|
|
382
|
-
}
|
|
383
|
-
: null,
|
|
384
|
-
recent_hypotheses: status.recentHypotheses.map((item) => ({
|
|
385
|
-
id: item.id,
|
|
386
|
-
statement: item.statement,
|
|
387
|
-
trigger: item.trigger,
|
|
388
|
-
created_at_ms: item.createdAtMs,
|
|
389
|
-
file: item.file,
|
|
390
|
-
strict_overall_score: typeof item.strictOverallScore === "number" ? item.strictOverallScore : null,
|
|
391
|
-
strict_decision: item.strictDecision ?? null,
|
|
392
|
-
})),
|
|
393
|
-
recent_change_stats: status.recentChangeStats.map((item) => ({
|
|
394
|
-
day: item.day,
|
|
395
|
-
run_id: item.runId,
|
|
396
|
-
new_count: item.newCount,
|
|
397
|
-
confirm_count: item.confirmCount,
|
|
398
|
-
revise_count: item.reviseCount,
|
|
399
|
-
bridge_count: item.bridgeCount,
|
|
400
|
-
})),
|
|
401
|
-
recent_papers: (projectRecentPapers.length > 0 ? projectRecentPapers : status.recentPapers).map((paper) => {
|
|
402
|
-
const paperId = typeof paper.id === "string" ? paper.id : "";
|
|
403
|
-
const fromGlobal = paperId ? globalById.get(paperId) : undefined;
|
|
404
|
-
return {
|
|
405
|
-
id: paperId || null,
|
|
406
|
-
title: paper.title ?? null,
|
|
407
|
-
url: paper.url ?? null,
|
|
408
|
-
last_score: "lastScore" in paper && typeof paper.lastScore === "number"
|
|
409
|
-
? paper.lastScore
|
|
410
|
-
: "score" in paper && typeof paper.score === "number"
|
|
411
|
-
? paper.score
|
|
412
|
-
: fromGlobal?.lastScore ?? null,
|
|
413
|
-
last_reason: "lastReason" in paper && typeof paper.lastReason === "string"
|
|
414
|
-
? paper.lastReason
|
|
415
|
-
: "reason" in paper && typeof paper.reason === "string"
|
|
416
|
-
? paper.reason
|
|
417
|
-
: fromGlobal?.lastReason ?? null,
|
|
418
|
-
first_pushed_at_ms: "firstPushedAtMs" in paper && typeof paper.firstPushedAtMs === "number"
|
|
419
|
-
? paper.firstPushedAtMs
|
|
420
|
-
: fromGlobal?.firstPushedAtMs ?? null,
|
|
421
|
-
last_pushed_at_ms: "lastPushedAtMs" in paper && typeof paper.lastPushedAtMs === "number"
|
|
422
|
-
? paper.lastPushedAtMs
|
|
423
|
-
: fromGlobal?.lastPushedAtMs ?? null,
|
|
424
|
-
push_count: "pushCount" in paper && typeof paper.pushCount === "number"
|
|
425
|
-
? paper.pushCount
|
|
426
|
-
: fromGlobal?.pushCount ?? null,
|
|
427
|
-
full_text_read: "fullTextRead" in paper && typeof paper.fullTextRead === "boolean"
|
|
428
|
-
? paper.fullTextRead
|
|
429
|
-
: null,
|
|
430
|
-
read_status: "readStatus" in paper && typeof paper.readStatus === "string"
|
|
431
|
-
? paper.readStatus
|
|
432
|
-
: null,
|
|
433
|
-
unread_reason: "unreadReason" in paper && typeof paper.unreadReason === "string"
|
|
434
|
-
? paper.unreadReason
|
|
435
|
-
: null,
|
|
436
|
-
evidence_anchors: "evidenceAnchors" in paper && Array.isArray(paper.evidenceAnchors)
|
|
437
|
-
? paper.evidenceAnchors.map((anchor) => ({
|
|
438
|
-
section: anchor && typeof anchor === "object" && "section" in anchor && typeof anchor.section === "string"
|
|
439
|
-
? anchor.section
|
|
440
|
-
: null,
|
|
441
|
-
locator: anchor && typeof anchor === "object" && "locator" in anchor && typeof anchor.locator === "string"
|
|
442
|
-
? anchor.locator
|
|
443
|
-
: null,
|
|
444
|
-
claim: anchor && typeof anchor === "object" && "claim" in anchor && typeof anchor.claim === "string"
|
|
445
|
-
? anchor.claim
|
|
446
|
-
: null,
|
|
447
|
-
quote: anchor && typeof anchor === "object" && "quote" in anchor && typeof anchor.quote === "string"
|
|
448
|
-
? anchor.quote
|
|
449
|
-
: null,
|
|
450
|
-
}))
|
|
451
|
-
: [],
|
|
452
|
-
};
|
|
453
|
-
}),
|
|
454
|
-
global_recent_papers: status.recentPapers.map((paper) => ({
|
|
455
|
-
id: paper.id,
|
|
456
|
-
title: paper.title ?? null,
|
|
457
|
-
url: paper.url ?? null,
|
|
458
|
-
last_score: paper.lastScore ?? null,
|
|
459
|
-
last_reason: paper.lastReason ?? null,
|
|
460
|
-
first_pushed_at_ms: paper.firstPushedAtMs,
|
|
461
|
-
last_pushed_at_ms: paper.lastPushedAtMs,
|
|
462
|
-
push_count: paper.pushCount,
|
|
463
|
-
})),
|
|
464
|
-
knowledge_state_missing_reason: status.knowledgeStateMissingReason ?? null,
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
function buildSubscribeArgs(params) {
|
|
468
|
-
const parts = [];
|
|
469
|
-
const schedule = normalizeScheduleInput(readStringParam(params, "schedule"), readBooleanParam(params, "run_now")) ??
|
|
470
|
-
"daily 09:00 Asia/Shanghai";
|
|
471
|
-
parts.push(schedule);
|
|
472
|
-
const resolved = resolveTopicAndMessage(params);
|
|
473
|
-
const topic = resolved.topic;
|
|
474
|
-
const message = resolved.message;
|
|
475
|
-
const scope = readStringParam(params, "scope");
|
|
476
161
|
if (topic) {
|
|
477
162
|
parts.push("--topic", quoteArg(topic));
|
|
478
163
|
}
|
|
479
164
|
const project = readStringParam(params, "project");
|
|
480
165
|
if (project) {
|
|
481
|
-
parts.push("--project", quoteArg(
|
|
166
|
+
parts.push("--project", quoteArg(project));
|
|
482
167
|
}
|
|
483
168
|
if (message) {
|
|
484
169
|
parts.push("--message", quoteArg(message));
|
|
485
170
|
}
|
|
486
|
-
const explicitLanguage = readStringParam(params, "language");
|
|
487
|
-
const languageCandidate = (() => {
|
|
488
|
-
if (explicitLanguage && ["zh", "en", "auto"].includes(explicitLanguage.toLowerCase())) {
|
|
489
|
-
return explicitLanguage.toLowerCase();
|
|
490
|
-
}
|
|
491
|
-
const channel = readStringParam(params, "channel");
|
|
492
|
-
if (channel && channel.toLowerCase() === "feishu")
|
|
493
|
-
return "zh";
|
|
494
|
-
if (scope && scope.toLowerCase().includes("feishu"))
|
|
495
|
-
return "zh";
|
|
496
|
-
const text = `${topic ?? ""} ${message ?? ""}`.trim();
|
|
497
|
-
if (/[\p{Script=Han}]/u.test(text))
|
|
498
|
-
return "zh";
|
|
499
|
-
return undefined;
|
|
500
|
-
})();
|
|
501
|
-
if (languageCandidate) {
|
|
502
|
-
parts.push("--language", quoteArg(languageCandidate));
|
|
503
|
-
}
|
|
504
171
|
const maxPapers = readNumberParam(params, "max_papers");
|
|
505
172
|
if (maxPapers !== undefined) {
|
|
506
173
|
parts.push("--max-papers", String(Math.floor(maxPapers)));
|
|
@@ -538,13 +205,6 @@ function buildSubscribeArgs(params) {
|
|
|
538
205
|
if (readBooleanParam(params, "no_deliver")) {
|
|
539
206
|
parts.push("--no-deliver");
|
|
540
207
|
}
|
|
541
|
-
if (readBooleanParam(params, "metadata_only")) {
|
|
542
|
-
parts.push("--metadata-only");
|
|
543
|
-
}
|
|
544
|
-
const language = readStringParam(params, "language");
|
|
545
|
-
if (language && ["zh", "en", "auto"].includes(language.toLowerCase())) {
|
|
546
|
-
parts.push("--language", quoteArg(language.toLowerCase()));
|
|
547
|
-
}
|
|
548
208
|
return parts.join(" ");
|
|
549
209
|
}
|
|
550
210
|
export function createScientifyCronTool(deps) {
|
|
@@ -558,184 +218,19 @@ export function createScientifyCronTool(deps) {
|
|
|
558
218
|
parameters: ScientifyCronToolSchema,
|
|
559
219
|
execute: async (_toolCallId, rawArgs) => {
|
|
560
220
|
const params = rawArgs;
|
|
561
|
-
const action =
|
|
562
|
-
if (!action) {
|
|
563
|
-
return Result.err("invalid_params", 'Unable to infer action. Use one of: action="upsert" | "list" | "remove".');
|
|
564
|
-
}
|
|
221
|
+
const action = (readStringParam(params, "action") ?? "").toLowerCase();
|
|
565
222
|
const scope = normalizeScope(readStringParam(params, "scope"));
|
|
566
223
|
try {
|
|
567
224
|
if (action === "upsert") {
|
|
568
|
-
|
|
569
|
-
// Default to no-deliver when delivery is unspecified to avoid hard failure on creation.
|
|
570
|
-
const hasDeliveryHints = Boolean(readStringParam(params, "channel")) || Boolean(readStringParam(params, "to"));
|
|
571
|
-
const upsertParams = readBooleanParam(params, "no_deliver") || hasDeliveryHints ? params : { ...params, no_deliver: true };
|
|
572
|
-
const args = buildSubscribeArgs(upsertParams);
|
|
225
|
+
const args = buildSubscribeArgs(params);
|
|
573
226
|
const ctx = buildToolContext(scope, args, `/research-subscribe ${args}`);
|
|
574
|
-
let expectedStateScopeKey;
|
|
575
|
-
try {
|
|
576
|
-
const parsed = parseSubscribeOptions(args);
|
|
577
|
-
if (!("error" in parsed)) {
|
|
578
|
-
const delivery = resolveDeliveryTarget(ctx, parsed);
|
|
579
|
-
if (!("error" in delivery)) {
|
|
580
|
-
expectedStateScopeKey = buildStateScopeKey(ctx, delivery);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
catch {
|
|
585
|
-
// keep best-effort behavior; fallback to parsed text scope or caller scope
|
|
586
|
-
}
|
|
587
227
|
const res = await subscribe(ctx);
|
|
588
228
|
const err = getResultError(res);
|
|
589
229
|
if (err) {
|
|
590
230
|
return Result.err("operation_failed", err);
|
|
591
231
|
}
|
|
592
232
|
const text = getResultText(res);
|
|
593
|
-
|
|
594
|
-
const resolved = resolveTopicAndMessage(upsertParams);
|
|
595
|
-
const incrementalScope = parseIncrementalScopeFromResultText(text);
|
|
596
|
-
const project = readStringParam(upsertParams, "project");
|
|
597
|
-
const runNow = readBooleanParam(upsertParams, "run_now");
|
|
598
|
-
if (runNow && jobId) {
|
|
599
|
-
const statusScopeCandidates = uniqueScopeCandidates([expectedStateScopeKey, incrementalScope, scope]);
|
|
600
|
-
const beforeStatusByScope = new Map();
|
|
601
|
-
if (resolved.topic) {
|
|
602
|
-
for (const statusScope of statusScopeCandidates) {
|
|
603
|
-
const before = await getIncrementalStateStatus({
|
|
604
|
-
scope: statusScope,
|
|
605
|
-
topic: resolved.topic,
|
|
606
|
-
...(project ? { projectId: project } : {}),
|
|
607
|
-
}).catch(() => undefined);
|
|
608
|
-
beforeStatusByScope.set(statusScope, before);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
const runArgsPrimary = [
|
|
612
|
-
"openclaw",
|
|
613
|
-
"cron",
|
|
614
|
-
"run",
|
|
615
|
-
jobId,
|
|
616
|
-
"--expect-final",
|
|
617
|
-
"--timeout",
|
|
618
|
-
"900000",
|
|
619
|
-
];
|
|
620
|
-
const runStartedAtMs = Date.now();
|
|
621
|
-
let runRes = await deps.runtime.system.runCommandWithTimeout(runArgsPrimary, {
|
|
622
|
-
timeoutMs: 920_000,
|
|
623
|
-
});
|
|
624
|
-
if (runRes.code !== 0 &&
|
|
625
|
-
/unknown option '--expect-final'|unknown option \"--expect-final\"|unknown option\s+--expect-final/i.test(runRes.stderr || "")) {
|
|
626
|
-
// Backward compatibility for older OpenClaw versions.
|
|
627
|
-
runRes = await deps.runtime.system.runCommandWithTimeout(["openclaw", "cron", "run", jobId], { timeoutMs: 600_000 });
|
|
628
|
-
}
|
|
629
|
-
let runAlreadyInProgress = false;
|
|
630
|
-
if (runRes.code !== 0) {
|
|
631
|
-
const marker = parseCronRunMarker(runRes.stdout) ?? parseCronRunMarker(runRes.stderr);
|
|
632
|
-
if (marker?.ok === true && marker?.ran === false && marker?.reason === "already-running") {
|
|
633
|
-
runAlreadyInProgress = true;
|
|
634
|
-
}
|
|
635
|
-
else {
|
|
636
|
-
return Result.err("operation_failed", runRes.stderr || runRes.stdout || `cron run failed for job ${jobId}`);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
let statusSnapshot;
|
|
640
|
-
if (resolved.topic) {
|
|
641
|
-
try {
|
|
642
|
-
let status;
|
|
643
|
-
let statusScopeUsed;
|
|
644
|
-
const deadline = Date.now() + (runAlreadyInProgress ? 300_000 : 120_000);
|
|
645
|
-
while (Date.now() <= deadline) {
|
|
646
|
-
for (const statusScope of statusScopeCandidates) {
|
|
647
|
-
const fetched = await getIncrementalStateStatus({
|
|
648
|
-
scope: statusScope,
|
|
649
|
-
topic: resolved.topic,
|
|
650
|
-
...(project ? { projectId: project } : {}),
|
|
651
|
-
}).catch(() => undefined);
|
|
652
|
-
const before = beforeStatusByScope.get(statusScope);
|
|
653
|
-
if (fetched && hasFreshRun(before, fetched, runStartedAtMs)) {
|
|
654
|
-
status = fetched;
|
|
655
|
-
statusScopeUsed = statusScope;
|
|
656
|
-
break;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
if (status) {
|
|
660
|
-
break;
|
|
661
|
-
}
|
|
662
|
-
await sleep(1_000);
|
|
663
|
-
}
|
|
664
|
-
if (!status) {
|
|
665
|
-
const fallbackError = "run_now completed but no new persisted research run was detected. Auto-persisted fallback error run.";
|
|
666
|
-
try {
|
|
667
|
-
const fallbackPersistErrors = [];
|
|
668
|
-
for (const [idx, statusScope] of statusScopeCandidates.entries()) {
|
|
669
|
-
const persisted = await recordIncrementalPush({
|
|
670
|
-
scope: statusScope,
|
|
671
|
-
topic: resolved.topic,
|
|
672
|
-
...(project ? { projectId: project } : {}),
|
|
673
|
-
status: "error",
|
|
674
|
-
runId: `${buildFallbackRunId(jobId)}-${idx + 1}`,
|
|
675
|
-
note: fallbackError,
|
|
676
|
-
papers: [],
|
|
677
|
-
knowledgeState: {
|
|
678
|
-
corePapers: [],
|
|
679
|
-
explorationPapers: [],
|
|
680
|
-
explorationTrace: [],
|
|
681
|
-
knowledgeChanges: [],
|
|
682
|
-
knowledgeUpdates: [],
|
|
683
|
-
hypotheses: [],
|
|
684
|
-
runLog: {
|
|
685
|
-
runProfile: readBooleanParam(upsertParams, "metadata_only") ? "fast" : "strict",
|
|
686
|
-
error: "run_now completed but the agent turn did not persist via scientify_literature_state.record",
|
|
687
|
-
notes: "Fallback persisted by scientify_cron_job guard to avoid stale status response.",
|
|
688
|
-
tempCleanupStatus: "not_needed",
|
|
689
|
-
},
|
|
690
|
-
},
|
|
691
|
-
}).catch((persistError) => {
|
|
692
|
-
fallbackPersistErrors.push(`${statusScope}:${persistError instanceof Error ? persistError.message : String(persistError)}`);
|
|
693
|
-
return undefined;
|
|
694
|
-
});
|
|
695
|
-
if (!persisted)
|
|
696
|
-
continue;
|
|
697
|
-
const fetched = await getIncrementalStateStatus({
|
|
698
|
-
scope: statusScope,
|
|
699
|
-
topic: resolved.topic,
|
|
700
|
-
...(project ? { projectId: project } : {}),
|
|
701
|
-
}).catch(() => undefined);
|
|
702
|
-
const before = beforeStatusByScope.get(statusScope);
|
|
703
|
-
if (fetched && hasFreshRun(before, fetched, runStartedAtMs)) {
|
|
704
|
-
status = fetched;
|
|
705
|
-
statusScopeUsed = statusScope;
|
|
706
|
-
break;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
if (!status) {
|
|
710
|
-
return Result.err("operation_failed", `${fallbackError} fresh_status_unavailable_after_fallback_persist. scopes=${statusScopeCandidates.join(",")} errors=${fallbackPersistErrors.join(" | ") || "none"}`);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
catch (persistError) {
|
|
714
|
-
return Result.err("operation_failed", `run_now completed but no new persisted research run was detected. Refusing stale status response. fallback_persist_error=${persistError instanceof Error ? persistError.message : String(persistError)}`);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
statusSnapshot = {
|
|
718
|
-
...serializeRunStatusSnapshot(status),
|
|
719
|
-
status_scope_used: statusScopeUsed ?? null,
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
catch (statusError) {
|
|
723
|
-
statusSnapshot = {
|
|
724
|
-
error: statusError instanceof Error ? statusError.message : String(statusError),
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
return Result.ok({
|
|
729
|
-
action,
|
|
730
|
-
scope,
|
|
731
|
-
job_id: jobId,
|
|
732
|
-
run_now: true,
|
|
733
|
-
run_status: runRes.code === 0 ? "ok" : "already-running",
|
|
734
|
-
...(statusSnapshot ? { status_json: statusSnapshot } : {}),
|
|
735
|
-
result: text,
|
|
736
|
-
});
|
|
737
|
-
}
|
|
738
|
-
return Result.ok({ action, scope, ...(jobId ? { job_id: jobId } : {}), result: text });
|
|
233
|
+
return Result.ok({ action, scope, result: text });
|
|
739
234
|
}
|
|
740
235
|
if (action === "list") {
|
|
741
236
|
const ctx = buildToolContext(scope, "", "/research-subscriptions");
|