@ontos-ai/knowhere-claw 0.1.0-beta.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -132
- package/dist/__tests__/channel-route.test.d.ts +1 -0
- package/dist/__tests__/ingest-tool.test.d.ts +1 -0
- package/dist/__tests__/read-result-file-tool.test.d.ts +1 -0
- package/dist/__tests__/tracker-progress.test.d.ts +1 -0
- package/dist/channel-delivery.d.ts +21 -0
- package/dist/channel-delivery.js +337 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +37 -23
- package/dist/index.js +13 -11
- package/dist/parser.js +1 -1
- package/dist/session.js +1 -0
- package/dist/store.d.ts +18 -1
- package/dist/store.js +91 -3
- package/dist/tools.d.ts +2 -3
- package/dist/tools.js +473 -105
- package/dist/tracker-progress.d.ts +3 -1
- package/dist/tracker-progress.js +8 -190
- package/dist/types.d.ts +7 -6
- package/openclaw.plugin.json +3 -21
- package/package.json +7 -5
- package/skills/knowhere/SKILL.md +57 -20
- package/dist/hooks.d.ts +0 -8
- package/dist/hooks.js +0 -415
package/README.md
CHANGED
|
@@ -1,137 +1,53 @@
|
|
|
1
|
-
# Knowhere OpenClaw
|
|
2
|
-
|
|
3
|
-
Developer guide for the Knowhere plugin that integrates with OpenClaw.
|
|
4
|
-
|
|
5
|
-
This repository is intentionally focused on runtime behavior, storage, and
|
|
6
|
-
integration details. Agent-facing usage guidance lives in
|
|
7
|
-
[`skills/knowhere/SKILL.md`](./skills/knowhere/SKILL.md).
|
|
8
|
-
|
|
9
|
-
## What this plugin does
|
|
10
|
-
|
|
11
|
-
The plugin uses Knowhere for parsing and job orchestration, then stores the
|
|
12
|
-
returned result package inside OpenClaw-managed local storage.
|
|
13
|
-
|
|
14
|
-
Its responsibilities are:
|
|
15
|
-
|
|
16
|
-
- register the `knowhere_*` tools
|
|
17
|
-
- optionally auto-ingest supported attachments
|
|
18
|
-
- persist extracted Knowhere result packages by scope
|
|
19
|
-
- expose browse-first path, chunk, context, and raw-file access back to agents
|
|
20
|
-
- inject compact document availability or status context when `autoGrounding` is enabled
|
|
21
|
-
|
|
22
|
-
The package targets Node `>=22.12.0` and builds to `dist/` with Rolldown plus
|
|
23
|
-
TypeScript declarations.
|
|
24
|
-
|
|
25
|
-
## Repository layout
|
|
26
|
-
|
|
27
|
-
- [`src/index.ts`](./src/index.ts): plugin entrypoint and registration
|
|
28
|
-
- [`src/config.ts`](./src/config.ts): config schema and resolution
|
|
29
|
-
- [`src/client.ts`](./src/client.ts): Knowhere API client
|
|
30
|
-
- [`src/store.ts`](./src/store.ts): scoped local storage and index maintenance
|
|
31
|
-
- [`src/parser.ts`](./src/parser.ts): ZIP extraction and stored result readers
|
|
32
|
-
- [`src/tools.ts`](./src/tools.ts): `knowhere_*` tool definitions and response formatting
|
|
33
|
-
- [`src/hooks.ts`](./src/hooks.ts): auto-grounding and background attachment ingest
|
|
34
|
-
- [`skills/knowhere/SKILL.md`](./skills/knowhere/SKILL.md): agent usage instructions
|
|
35
|
-
- [`smoketest/run-tool.ts`](./smoketest/run-tool.ts): tool smoke-test entrypoint
|
|
36
|
-
- [`openclaw.plugin.json`](./openclaw.plugin.json): plugin manifest, config schema, and bundled skill declaration
|
|
37
|
-
|
|
38
|
-
## Storage model
|
|
39
|
-
|
|
40
|
-
The store is scope-aware. Supported `scopeMode` values are `session`, `agent`,
|
|
41
|
-
and `global`.
|
|
42
|
-
|
|
43
|
-
Each scope is stored under the resolved plugin storage directory with this
|
|
44
|
-
shape:
|
|
45
|
-
|
|
46
|
-
```text
|
|
47
|
-
<scope>/
|
|
48
|
-
index.json
|
|
49
|
-
documents/
|
|
50
|
-
<docId>/
|
|
51
|
-
metadata.json
|
|
52
|
-
browse-index.json
|
|
53
|
-
result/
|
|
54
|
-
manifest.json
|
|
55
|
-
chunks.json
|
|
56
|
-
hierarchy.json
|
|
57
|
-
full.md
|
|
58
|
-
...
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Storage roles:
|
|
62
|
-
|
|
63
|
-
- `index.json`: internal per-scope cache of document summaries; rebuildable
|
|
64
|
-
- `metadata.json`: plugin-local mapping layer for one stored document
|
|
65
|
-
- `browse-index.json`: plugin-local browse index for path navigation and result-file inventory
|
|
66
|
-
- `result/`: untouched extracted Knowhere result package files
|
|
67
|
-
|
|
68
|
-
`index.json` is an optimization, not the primary source of truth. If its schema
|
|
69
|
-
version no longer matches the current store code, the plugin rebuilds it from
|
|
70
|
-
the per-document directories.
|
|
1
|
+
# Knowhere for OpenClaw
|
|
71
2
|
|
|
72
|
-
|
|
3
|
+
Knowhere is an OpenClaw plugin that parses documents and URLs with Knowhere,
|
|
4
|
+
stores the extracted result package in OpenClaw state, and gives agents a
|
|
5
|
+
browse-first toolset for grounded document work.
|
|
73
6
|
|
|
74
|
-
|
|
7
|
+
Quick mental model:
|
|
75
8
|
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
9
|
+
- Install the plugin
|
|
10
|
+
- Restart the Gateway
|
|
11
|
+
- Configure `plugins.entries."knowhere-claw".config`
|
|
12
|
+
- Ask an agent to read, search, or compare a document
|
|
13
|
+
- Let the bundled `knowhere` skill steer the agent toward the right tools
|
|
79
14
|
|
|
80
|
-
|
|
15
|
+
## Where It Runs
|
|
81
16
|
|
|
82
|
-
|
|
83
|
-
2. `knowhere_preview_document`
|
|
84
|
-
3. `knowhere_grep` for text search
|
|
85
|
-
4. `knowhere_read_result_file` again for `hierarchy.json`, `kb.csv`, or table HTML when needed
|
|
17
|
+
This plugin runs inside the OpenClaw Gateway process.
|
|
86
18
|
|
|
87
|
-
|
|
88
|
-
|
|
19
|
+
If your agents talk to a remote Gateway, install and configure the plugin on
|
|
20
|
+
the machine running that Gateway, then restart that Gateway.
|
|
89
21
|
|
|
90
|
-
##
|
|
22
|
+
## What You Get
|
|
91
23
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
24
|
+
- Ingest local files or document URLs with Knowhere
|
|
25
|
+
- Store parsed result packages inside OpenClaw-managed state
|
|
26
|
+
- Preview document structure, search chunks, and inspect raw result files
|
|
27
|
+
- Reuse stored documents across `session`, `agent`, or `global` scope
|
|
28
|
+
- Ship a bundled `knowhere` skill so agents prefer this toolchain for
|
|
29
|
+
document-heavy tasks
|
|
97
30
|
|
|
98
|
-
|
|
31
|
+
## Install
|
|
99
32
|
|
|
100
33
|
```bash
|
|
101
|
-
|
|
102
|
-
pnpm typecheck
|
|
103
|
-
pnpm lint
|
|
104
|
-
pnpm build
|
|
34
|
+
openclaw plugins install @ontos-ai/knowhere-claw
|
|
105
35
|
```
|
|
106
36
|
|
|
107
|
-
|
|
37
|
+
Restart the Gateway afterwards.
|
|
108
38
|
|
|
109
|
-
|
|
110
|
-
- `pnpm fmt`: format the repository with Oxc Formatter
|
|
111
|
-
- `pnpm fmt:check`: check formatting without writing changes
|
|
112
|
-
- `pnpm lint`: run Oxlint in type-aware mode
|
|
113
|
-
- `pnpm lint:fix`: run Oxlint with fixes
|
|
114
|
-
- `pnpm typecheck`: run `tsgo --noEmit`
|
|
115
|
-
- `pnpm smoke:tools -- ...`: execute one plugin tool through the real registration path
|
|
116
|
-
- `pnpm clean`: remove `dist/`
|
|
39
|
+
## Config
|
|
117
40
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
Minimal plugin config:
|
|
41
|
+
Set config under `plugins.entries."knowhere-claw".config`:
|
|
121
42
|
|
|
122
43
|
```json5
|
|
123
44
|
{
|
|
124
45
|
plugins: {
|
|
125
|
-
load: {
|
|
126
|
-
paths: ["/absolute/path/to/knowhere-openclaw-plugin"],
|
|
127
|
-
},
|
|
128
46
|
entries: {
|
|
129
|
-
knowhere: {
|
|
47
|
+
"knowhere-claw": {
|
|
130
48
|
enabled: true,
|
|
131
49
|
config: {
|
|
132
|
-
apiKey
|
|
133
|
-
scopeMode: "session",
|
|
134
|
-
autoGrounding: true,
|
|
50
|
+
// apiKey is optional
|
|
135
51
|
},
|
|
136
52
|
},
|
|
137
53
|
},
|
|
@@ -141,23 +57,80 @@ Minimal plugin config:
|
|
|
141
57
|
|
|
142
58
|
Config notes:
|
|
143
59
|
|
|
144
|
-
- `apiKey
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
- `
|
|
158
|
-
|
|
159
|
-
-
|
|
160
|
-
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
60
|
+
- `apiKey`: optional Knowhere API key. If omitted, the plugin falls back to
|
|
61
|
+
`KNOWHERE_API_KEY` and any API key previously stored with
|
|
62
|
+
`knowhere_set_api_key`.
|
|
63
|
+
- If no API key is available when the agent first needs Knowhere, the plugin
|
|
64
|
+
guides the user to the Knowhere API key page at
|
|
65
|
+
`https://knowhereto.ai/api-keys`.
|
|
66
|
+
- `baseUrl`: optional Knowhere API base URL. Falls back to
|
|
67
|
+
`KNOWHERE_BASE_URL` and defaults to `https://api.knowhereto.ai`.
|
|
68
|
+
- `storageDir`: optional directory for persisted parsed documents. By default,
|
|
69
|
+
the plugin stores data under the OpenClaw state directory for
|
|
70
|
+
`knowhere-claw`.
|
|
71
|
+
- `scopeMode`: document sharing boundary. Supported values are `session`,
|
|
72
|
+
`agent`, and `global`. If omitted, the plugin defaults to `session`.
|
|
73
|
+
- `pollIntervalMs`, `pollTimeoutMs`, `requestTimeoutMs`, `uploadTimeoutMs`:
|
|
74
|
+
optional tuning for job polling, API calls, and large uploads.
|
|
75
|
+
- An explicit `storageDir` such as
|
|
76
|
+
`/home/<user>/.openclaw/plugin-state/knowhere` makes stored result packages
|
|
77
|
+
easier to inspect, back up, or clean up.
|
|
78
|
+
|
|
79
|
+
## How OpenClaw Uses It
|
|
80
|
+
|
|
81
|
+
Once the plugin is enabled, you can ask an OpenClaw agent to:
|
|
82
|
+
|
|
83
|
+
- summarize a local document
|
|
84
|
+
- read a document URL
|
|
85
|
+
- find a table or section in a previously ingested document
|
|
86
|
+
- compare a new document with documents already stored in scope
|
|
87
|
+
|
|
88
|
+
The bundled `knowhere` skill teaches agents to use the `knowhere_*` tools
|
|
89
|
+
instead of raw file reads when document parsing matters.
|
|
90
|
+
|
|
91
|
+
If you use skill filters or allowlists in OpenClaw, keep the bundled
|
|
92
|
+
`knowhere` skill enabled or the tools will load without their intended usage
|
|
93
|
+
guidance.
|
|
94
|
+
|
|
95
|
+
If your agent runtime uses a tool allowlist, include `knowhere_*` so agents can
|
|
96
|
+
actually call the plugin tools.
|
|
97
|
+
|
|
98
|
+
## Scope And Storage
|
|
99
|
+
|
|
100
|
+
`scopeMode` controls who can reuse parsed documents:
|
|
101
|
+
|
|
102
|
+
- `session`: only the current session can reuse the stored documents
|
|
103
|
+
- `agent`: all conversations for the same agent can reuse them
|
|
104
|
+
- `global`: all agents on the same Gateway can reuse them
|
|
105
|
+
|
|
106
|
+
Within each scope, the plugin keeps:
|
|
107
|
+
|
|
108
|
+
- an `index.json` cache of stored document summaries
|
|
109
|
+
- per-document metadata and browse indexes
|
|
110
|
+
- the extracted Knowhere result package under `result/`
|
|
111
|
+
|
|
112
|
+
## Common Workflow
|
|
113
|
+
|
|
114
|
+
1. Provide a file path or URL to the agent.
|
|
115
|
+
2. The agent ingests it into Knowhere and waits for the parse to finish.
|
|
116
|
+
3. Follow-up questions reuse stored results from the current scope.
|
|
117
|
+
4. When needed, the agent can preview structure, search chunks, read raw result
|
|
118
|
+
files, or clear stored documents.
|
|
119
|
+
|
|
120
|
+
## Troubleshooting
|
|
121
|
+
|
|
122
|
+
- Missing API key: `apiKey` config is optional. You can set
|
|
123
|
+
`plugins.entries."knowhere-claw".config.apiKey`, export
|
|
124
|
+
`KNOWHERE_API_KEY`, or let the plugin guide the user to
|
|
125
|
+
`https://knowhereto.ai/api-keys` and store the key with
|
|
126
|
+
`knowhere_set_api_key`.
|
|
127
|
+
- Documents are not shared between conversations: change `scopeMode` from
|
|
128
|
+
`session` to `agent` or `global`.
|
|
129
|
+
- The plugin is enabled but agents do not reach for Knowhere: check that the
|
|
130
|
+
bundled `knowhere` skill is not filtered out.
|
|
131
|
+
- Large documents time out: increase `pollTimeoutMs` or `uploadTimeoutMs`.
|
|
132
|
+
|
|
133
|
+
## Developer Docs
|
|
134
|
+
|
|
135
|
+
Contributor-oriented architecture, workflow, and packaging notes live in
|
|
136
|
+
`DEVELOPMENT.md` at the repository root.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import type { ChannelRouteRecord } from "./types";
|
|
3
|
+
type DeliverySurface = "discord" | "imessage" | "line" | "signal" | "slack" | "telegram" | "whatsapp";
|
|
4
|
+
type DeliveryResult = {
|
|
5
|
+
delivered: boolean;
|
|
6
|
+
surface?: DeliverySurface;
|
|
7
|
+
to?: string;
|
|
8
|
+
accountId?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function deliverChannelMessage(params: {
|
|
11
|
+
api: OpenClawPluginApi;
|
|
12
|
+
operationLabel: string;
|
|
13
|
+
context?: unknown;
|
|
14
|
+
sessionKey?: string;
|
|
15
|
+
messages?: unknown[];
|
|
16
|
+
channelRoute?: ChannelRouteRecord;
|
|
17
|
+
text: string;
|
|
18
|
+
mediaUrl?: string;
|
|
19
|
+
mediaLocalRoots?: readonly string[];
|
|
20
|
+
}): Promise<DeliveryResult>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { isRecord } from "./types.js";
|
|
2
|
+
import { normalizeWhitespace } from "./text.js";
|
|
3
|
+
import { findConversationSegmentValue, parseConversationSessionKey } from "./session.js";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
//#region src/channel-delivery.ts
|
|
6
|
+
const DELIVERY_SURFACES = new Set([
|
|
7
|
+
"discord",
|
|
8
|
+
"imessage",
|
|
9
|
+
"line",
|
|
10
|
+
"signal",
|
|
11
|
+
"slack",
|
|
12
|
+
"telegram",
|
|
13
|
+
"whatsapp"
|
|
14
|
+
]);
|
|
15
|
+
function readString(value) {
|
|
16
|
+
return normalizeWhitespace(value) || void 0;
|
|
17
|
+
}
|
|
18
|
+
function readInteger(value) {
|
|
19
|
+
const normalized = readString(value);
|
|
20
|
+
if (!normalized || !/^-?\d+$/.test(normalized)) return;
|
|
21
|
+
const parsed = Number(normalized);
|
|
22
|
+
return Number.isSafeInteger(parsed) ? parsed : void 0;
|
|
23
|
+
}
|
|
24
|
+
function normalizeDeliverySurface(value) {
|
|
25
|
+
const normalized = readString(value)?.toLowerCase();
|
|
26
|
+
if (!normalized || !DELIVERY_SURFACES.has(normalized)) return;
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
function readThreadValue(value) {
|
|
30
|
+
if (typeof value === "number" && Number.isFinite(value)) return Math.trunc(value);
|
|
31
|
+
return readString(value);
|
|
32
|
+
}
|
|
33
|
+
function parseSessionThreadInfo(sessionKey) {
|
|
34
|
+
const normalized = readString(sessionKey);
|
|
35
|
+
if (!normalized) return {};
|
|
36
|
+
const topicIndex = normalized.lastIndexOf(":topic:");
|
|
37
|
+
const threadIndex = normalized.lastIndexOf(":thread:");
|
|
38
|
+
const markerIndex = Math.max(topicIndex, threadIndex);
|
|
39
|
+
if (markerIndex < 0) return { baseSessionKey: normalized };
|
|
40
|
+
const marker = topicIndex > threadIndex ? ":topic:" : ":thread:";
|
|
41
|
+
return {
|
|
42
|
+
baseSessionKey: normalized.slice(0, markerIndex),
|
|
43
|
+
threadId: normalized.slice(markerIndex + marker.length).trim() || void 0
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function buildSessionStoreLookupKeys(sessionKey) {
|
|
47
|
+
const normalized = readString(sessionKey);
|
|
48
|
+
if (!normalized) return [];
|
|
49
|
+
const { baseSessionKey } = parseSessionThreadInfo(normalized);
|
|
50
|
+
const candidates = [
|
|
51
|
+
normalized,
|
|
52
|
+
normalized.toLowerCase(),
|
|
53
|
+
baseSessionKey,
|
|
54
|
+
baseSessionKey?.toLowerCase()
|
|
55
|
+
];
|
|
56
|
+
return [...new Set(candidates.filter((value) => Boolean(value)))];
|
|
57
|
+
}
|
|
58
|
+
function readSessionDelivery(value) {
|
|
59
|
+
const entry = isRecord(value) ? value : void 0;
|
|
60
|
+
const deliveryContext = isRecord(entry?.deliveryContext) ? entry.deliveryContext : void 0;
|
|
61
|
+
const surface = normalizeDeliverySurface(deliveryContext?.channel) || normalizeDeliverySurface(entry?.lastChannel) || normalizeDeliverySurface(entry?.channel);
|
|
62
|
+
const to = readString(deliveryContext?.to) || readString(entry?.lastTo);
|
|
63
|
+
const accountId = readString(deliveryContext?.accountId) || readString(entry?.lastAccountId);
|
|
64
|
+
const threadId = readThreadValue(deliveryContext?.threadId ?? entry?.lastThreadId);
|
|
65
|
+
if (!surface && !to && !accountId && threadId == null) return;
|
|
66
|
+
return {
|
|
67
|
+
surface,
|
|
68
|
+
to,
|
|
69
|
+
accountId,
|
|
70
|
+
threadId
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function resolveSessionDelivery(params) {
|
|
74
|
+
const sessionKey = readString(params.sessionKey);
|
|
75
|
+
if (!sessionKey) return;
|
|
76
|
+
try {
|
|
77
|
+
const parsedSession = parseConversationSessionKey(sessionKey);
|
|
78
|
+
const storePath = params.api.runtime.channel.session.resolveStorePath(params.api.config.session?.store, { agentId: parsedSession?.agentId });
|
|
79
|
+
const lookupKeys = buildSessionStoreLookupKeys(sessionKey);
|
|
80
|
+
params.api.logger.debug?.(`knowhere: ${params.operationLabel} session delivery lookup sessionKey=${sessionKey} storePath=${storePath} lookupKeys=${JSON.stringify(lookupKeys)}`);
|
|
81
|
+
const rawStore = await fs.readFile(storePath, "utf-8");
|
|
82
|
+
const store = JSON.parse(rawStore);
|
|
83
|
+
if (!isRecord(store)) {
|
|
84
|
+
params.api.logger.debug?.(`knowhere: ${params.operationLabel} session delivery store invalid sessionKey=${sessionKey} storePath=${storePath}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
for (const lookupKey of lookupKeys) {
|
|
88
|
+
const delivery = readSessionDelivery(store[lookupKey]);
|
|
89
|
+
if (delivery) {
|
|
90
|
+
params.api.logger.debug?.(`knowhere: ${params.operationLabel} session delivery resolved sessionKey=${sessionKey} matchedKey=${lookupKey} surface=${delivery.surface ?? "(none)"} to=${delivery.to ?? "(none)"} accountId=${delivery.accountId ?? "(none)"} threadId=${delivery.threadId ?? "(none)"}`);
|
|
91
|
+
return delivery;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
params.api.logger.debug?.(`knowhere: ${params.operationLabel} session delivery not found sessionKey=${sessionKey} storePath=${storePath}`);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
params.api.logger.debug?.(`knowhere: ${params.operationLabel} session delivery lookup failed${sessionKey ? ` sessionKey=${sessionKey}` : ""}. ${error instanceof Error ? error.message : String(error)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function extractReplyReferenceFromValue(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
100
|
+
if (typeof value === "string") return readString(value.match(/"(?:messageId|message_id|replyTo|reply_to|threadTs|thread_ts|ts)"\s*:\s*"([^"]+)"/)?.[1]);
|
|
101
|
+
if (Array.isArray(value)) {
|
|
102
|
+
if (seen.has(value)) return;
|
|
103
|
+
seen.add(value);
|
|
104
|
+
for (let index = value.length - 1; index >= 0; index -= 1) {
|
|
105
|
+
const replyReference = extractReplyReferenceFromValue(value[index], seen);
|
|
106
|
+
if (replyReference) return replyReference;
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!isRecord(value)) return;
|
|
111
|
+
if (seen.has(value)) return;
|
|
112
|
+
seen.add(value);
|
|
113
|
+
for (const key of [
|
|
114
|
+
"messageId",
|
|
115
|
+
"message_id",
|
|
116
|
+
"replyTo",
|
|
117
|
+
"reply_to",
|
|
118
|
+
"threadTs",
|
|
119
|
+
"thread_ts",
|
|
120
|
+
"ts"
|
|
121
|
+
]) {
|
|
122
|
+
const replyReference = readString(value[key]);
|
|
123
|
+
if (replyReference) return replyReference;
|
|
124
|
+
}
|
|
125
|
+
const preferredKeys = [
|
|
126
|
+
"content",
|
|
127
|
+
"text",
|
|
128
|
+
"body",
|
|
129
|
+
"message",
|
|
130
|
+
"messages",
|
|
131
|
+
"items"
|
|
132
|
+
];
|
|
133
|
+
const preferredKeySet = new Set(preferredKeys);
|
|
134
|
+
for (const key of preferredKeys) {
|
|
135
|
+
const replyReference = extractReplyReferenceFromValue(value[key], seen);
|
|
136
|
+
if (replyReference) return replyReference;
|
|
137
|
+
}
|
|
138
|
+
for (const [key, nestedValue] of Object.entries(value).reverse()) {
|
|
139
|
+
if (preferredKeySet.has(key)) continue;
|
|
140
|
+
const replyReference = extractReplyReferenceFromValue(nestedValue, seen);
|
|
141
|
+
if (replyReference) return replyReference;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function resolveDeliveryTarget(params) {
|
|
145
|
+
const context = isRecord(params.context) ? params.context : void 0;
|
|
146
|
+
const session = parseConversationSessionKey(params.sessionKey);
|
|
147
|
+
const surface = normalizeDeliverySurface(context?.channelId) || normalizeDeliverySurface(context?.provider) || normalizeDeliverySurface(context?.surface) || normalizeDeliverySurface(params.channelRoute?.channelId) || params.sessionDelivery?.surface || normalizeDeliverySurface(session?.surface);
|
|
148
|
+
if (!surface) return;
|
|
149
|
+
const replyReference = readString(context?.messageId) || readString(context?.replyTo) || readString(context?.threadTs) || (params.messages ? extractReplyReferenceFromValue(params.messages) : void 0);
|
|
150
|
+
switch (surface) {
|
|
151
|
+
case "discord": {
|
|
152
|
+
const threadId = readString(context?.threadId) || findConversationSegmentValue(session, "thread") || readString(params.sessionDelivery?.threadId);
|
|
153
|
+
const channelId = findConversationSegmentValue(session, "channel", "guild");
|
|
154
|
+
const directId = findConversationSegmentValue(session, "direct", "user", "member");
|
|
155
|
+
const to = threadId && `channel:${threadId}` || readString(context?.conversationId) || params.channelRoute?.conversationId || params.sessionDelivery?.to || channelId && `channel:${channelId}` || directId && `user:${directId}`;
|
|
156
|
+
if (!to) return;
|
|
157
|
+
return {
|
|
158
|
+
surface,
|
|
159
|
+
to,
|
|
160
|
+
accountId: readString(context?.accountId) || findConversationSegmentValue(session, "account") || params.channelRoute?.accountId || params.sessionDelivery?.accountId,
|
|
161
|
+
replyTo: replyReference
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
case "slack": {
|
|
165
|
+
const directId = findConversationSegmentValue(session, "direct", "user", "member");
|
|
166
|
+
const channelId = findConversationSegmentValue(session, "channel", "group");
|
|
167
|
+
const to = readString(context?.conversationId) || params.channelRoute?.conversationId || params.sessionDelivery?.to || channelId || (directId ? `user:${directId}` : void 0);
|
|
168
|
+
if (!to) return;
|
|
169
|
+
return {
|
|
170
|
+
surface,
|
|
171
|
+
to,
|
|
172
|
+
accountId: readString(context?.accountId) || findConversationSegmentValue(session, "account") || params.channelRoute?.accountId || params.sessionDelivery?.accountId,
|
|
173
|
+
threadTs: readString(context?.threadTs) || replyReference || readString(params.sessionDelivery?.threadId)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
case "telegram": {
|
|
177
|
+
const chatId = findConversationSegmentValue(session, "direct", "group", "channel", "chat", "room");
|
|
178
|
+
const to = readString(context?.conversationId) || params.channelRoute?.conversationId || params.sessionDelivery?.to || chatId;
|
|
179
|
+
if (!to) return;
|
|
180
|
+
return {
|
|
181
|
+
surface,
|
|
182
|
+
to,
|
|
183
|
+
accountId: readString(context?.accountId) || findConversationSegmentValue(session, "account") || params.channelRoute?.accountId || params.sessionDelivery?.accountId,
|
|
184
|
+
replyToMessageId: readInteger(replyReference),
|
|
185
|
+
messageThreadId: readInteger(context?.threadId) ?? (typeof params.sessionDelivery?.threadId === "number" ? params.sessionDelivery.threadId : readInteger(params.sessionDelivery?.threadId))
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
case "line": {
|
|
189
|
+
const directId = findConversationSegmentValue(session, "direct", "user", "member");
|
|
190
|
+
const groupId = findConversationSegmentValue(session, "group");
|
|
191
|
+
const roomId = findConversationSegmentValue(session, "room");
|
|
192
|
+
const chatId = findConversationSegmentValue(session, "channel", "chat", "space");
|
|
193
|
+
const to = readString(context?.conversationId) || params.channelRoute?.conversationId || params.sessionDelivery?.to || (groupId ? `line:group:${groupId}` : void 0) || (roomId ? `line:room:${roomId}` : void 0) || (directId ? `line:${directId}` : void 0) || (chatId ? `line:${chatId}` : void 0);
|
|
194
|
+
if (!to) return;
|
|
195
|
+
return {
|
|
196
|
+
surface,
|
|
197
|
+
to,
|
|
198
|
+
accountId: readString(context?.accountId) || findConversationSegmentValue(session, "account") || params.channelRoute?.accountId || params.sessionDelivery?.accountId,
|
|
199
|
+
replyToken: readString(context?.replyToken)
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
case "imessage":
|
|
203
|
+
case "signal":
|
|
204
|
+
case "whatsapp": {
|
|
205
|
+
const conversationId = findConversationSegmentValue(session, "direct", "group", "channel", "chat", "room");
|
|
206
|
+
const to = readString(context?.conversationId) || params.channelRoute?.conversationId || params.sessionDelivery?.to || conversationId;
|
|
207
|
+
if (!to) return;
|
|
208
|
+
return {
|
|
209
|
+
surface,
|
|
210
|
+
to,
|
|
211
|
+
accountId: readString(context?.accountId) || findConversationSegmentValue(session, "account") || params.channelRoute?.accountId || params.sessionDelivery?.accountId
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function hasDeliveryResolutionHints(params) {
|
|
217
|
+
const context = isRecord(params.context) ? params.context : void 0;
|
|
218
|
+
if (normalizeDeliverySurface(context?.channelId) || normalizeDeliverySurface(context?.provider) || normalizeDeliverySurface(context?.surface) || normalizeDeliverySurface(params.channelRoute?.channelId) || parseConversationSessionKey(params.sessionKey)) return true;
|
|
219
|
+
return Boolean(readString(context?.conversationId) || readString(context?.threadId) || readString(context?.groupId) || readString(context?.messageId) || readString(context?.replyTo) || readString(context?.threadTs) || params.channelRoute?.conversationId);
|
|
220
|
+
}
|
|
221
|
+
function canSendMediaDirectly(target, mediaUrl) {
|
|
222
|
+
if (target.surface !== "line") return true;
|
|
223
|
+
return /^https?:\/\//i.test(mediaUrl.trim());
|
|
224
|
+
}
|
|
225
|
+
async function deliverChannelMessage(params) {
|
|
226
|
+
const sessionDelivery = await resolveSessionDelivery({
|
|
227
|
+
api: params.api,
|
|
228
|
+
sessionKey: params.sessionKey,
|
|
229
|
+
operationLabel: params.operationLabel
|
|
230
|
+
});
|
|
231
|
+
const target = resolveDeliveryTarget({
|
|
232
|
+
context: params.context,
|
|
233
|
+
sessionKey: params.sessionKey,
|
|
234
|
+
messages: params.messages,
|
|
235
|
+
channelRoute: params.channelRoute,
|
|
236
|
+
sessionDelivery
|
|
237
|
+
});
|
|
238
|
+
params.api.logger.debug?.(`knowhere: ${params.operationLabel} resolution sessionKey=${readString(params.sessionKey) ?? "(none)"} routeAccountId=${params.channelRoute?.accountId ?? "(none)"} routeConversationId=${params.channelRoute?.conversationId ?? "(none)"} sessionStoreAccountId=${sessionDelivery?.accountId ?? "(none)"} sessionStoreTo=${sessionDelivery?.to ?? "(none)"} resolvedSurface=${target?.surface ?? "(none)"} resolvedTo=${target?.to ?? "(none)"} resolvedAccountId=${target?.accountId ?? "(none)"}`);
|
|
239
|
+
if (!target) {
|
|
240
|
+
if (hasDeliveryResolutionHints({
|
|
241
|
+
context: params.context,
|
|
242
|
+
sessionKey: params.sessionKey,
|
|
243
|
+
channelRoute: params.channelRoute
|
|
244
|
+
})) {
|
|
245
|
+
const sessionKey = readString(params.sessionKey);
|
|
246
|
+
params.api.logger.warn(`knowhere: ${params.operationLabel} skipped because no target could be resolved${sessionKey ? ` sessionKey=${sessionKey}` : ""}`);
|
|
247
|
+
}
|
|
248
|
+
return { delivered: false };
|
|
249
|
+
}
|
|
250
|
+
const mediaUrl = readString(params.mediaUrl);
|
|
251
|
+
if (mediaUrl && !canSendMediaDirectly(target, mediaUrl)) {
|
|
252
|
+
params.api.logger.info(`knowhere: ${params.operationLabel} direct media delivery not supported for ${target.surface}; falling back`);
|
|
253
|
+
return {
|
|
254
|
+
delivered: false,
|
|
255
|
+
surface: target.surface,
|
|
256
|
+
to: target.to,
|
|
257
|
+
accountId: target.accountId
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
params.api.logger.info(`knowhere: ${params.operationLabel} sending via ${target.surface} to ${target.to}${target.accountId ? ` accountId=${target.accountId}` : ""}${mediaUrl ? " media=yes" : " media=no"}`);
|
|
262
|
+
switch (target.surface) {
|
|
263
|
+
case "discord":
|
|
264
|
+
await params.api.runtime.channel.discord.sendMessageDiscord(target.to, params.text, {
|
|
265
|
+
accountId: target.accountId,
|
|
266
|
+
replyTo: target.replyTo,
|
|
267
|
+
...mediaUrl ? { mediaUrl } : {},
|
|
268
|
+
...params.mediaLocalRoots?.length ? { mediaLocalRoots: params.mediaLocalRoots } : {}
|
|
269
|
+
});
|
|
270
|
+
break;
|
|
271
|
+
case "slack":
|
|
272
|
+
await params.api.runtime.channel.slack.sendMessageSlack(target.to, params.text, {
|
|
273
|
+
accountId: target.accountId,
|
|
274
|
+
threadTs: target.threadTs,
|
|
275
|
+
...mediaUrl ? { mediaUrl } : {},
|
|
276
|
+
...params.mediaLocalRoots?.length ? { mediaLocalRoots: params.mediaLocalRoots } : {}
|
|
277
|
+
});
|
|
278
|
+
break;
|
|
279
|
+
case "telegram":
|
|
280
|
+
await params.api.runtime.channel.telegram.sendMessageTelegram(target.to, params.text, {
|
|
281
|
+
accountId: target.accountId,
|
|
282
|
+
messageThreadId: target.messageThreadId,
|
|
283
|
+
replyToMessageId: target.replyToMessageId,
|
|
284
|
+
...mediaUrl ? { mediaUrl } : {},
|
|
285
|
+
...params.mediaLocalRoots?.length ? { mediaLocalRoots: params.mediaLocalRoots } : {}
|
|
286
|
+
});
|
|
287
|
+
break;
|
|
288
|
+
case "line":
|
|
289
|
+
await params.api.runtime.channel.line.sendMessageLine(target.to, params.text, {
|
|
290
|
+
accountId: target.accountId,
|
|
291
|
+
replyToken: target.replyToken,
|
|
292
|
+
verbose: false,
|
|
293
|
+
...mediaUrl ? { mediaUrl } : {}
|
|
294
|
+
});
|
|
295
|
+
break;
|
|
296
|
+
case "whatsapp":
|
|
297
|
+
await params.api.runtime.channel.whatsapp.sendMessageWhatsApp(target.to, params.text, {
|
|
298
|
+
accountId: target.accountId,
|
|
299
|
+
verbose: false,
|
|
300
|
+
...mediaUrl ? { mediaUrl } : {},
|
|
301
|
+
...params.mediaLocalRoots?.length ? { mediaLocalRoots: params.mediaLocalRoots } : {}
|
|
302
|
+
});
|
|
303
|
+
break;
|
|
304
|
+
case "signal":
|
|
305
|
+
await params.api.runtime.channel.signal.sendMessageSignal(target.to, params.text, {
|
|
306
|
+
accountId: target.accountId,
|
|
307
|
+
...mediaUrl ? { mediaUrl } : {},
|
|
308
|
+
...params.mediaLocalRoots?.length ? { mediaLocalRoots: params.mediaLocalRoots } : {}
|
|
309
|
+
});
|
|
310
|
+
break;
|
|
311
|
+
case "imessage":
|
|
312
|
+
await params.api.runtime.channel.imessage.sendMessageIMessage(target.to, params.text, {
|
|
313
|
+
accountId: target.accountId,
|
|
314
|
+
...mediaUrl ? { mediaUrl } : {},
|
|
315
|
+
...params.mediaLocalRoots?.length ? { mediaLocalRoots: params.mediaLocalRoots } : {}
|
|
316
|
+
});
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
params.api.logger.info(`knowhere: ${params.operationLabel} sent via ${target.surface} to ${target.to}${target.accountId ? ` accountId=${target.accountId}` : ""}`);
|
|
320
|
+
return {
|
|
321
|
+
delivered: true,
|
|
322
|
+
surface: target.surface,
|
|
323
|
+
to: target.to,
|
|
324
|
+
accountId: target.accountId
|
|
325
|
+
};
|
|
326
|
+
} catch (error) {
|
|
327
|
+
params.api.logger.warn(`knowhere: ${params.operationLabel} send failed via ${target.surface} to ${target.to}${target.accountId ? ` accountId=${target.accountId}` : ""}. ${error instanceof Error ? error.message : String(error)}`);
|
|
328
|
+
return {
|
|
329
|
+
delivered: false,
|
|
330
|
+
surface: target.surface,
|
|
331
|
+
to: target.to,
|
|
332
|
+
accountId: target.accountId
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
//#endregion
|
|
337
|
+
export { deliverChannelMessage };
|
package/dist/config.d.ts
CHANGED
|
@@ -3,4 +3,10 @@ import type { JsonSchemaObject, ResolvedKnowhereConfig } from "./types";
|
|
|
3
3
|
export declare const DEFAULT_BASE_URL = "https://api.knowhereto.ai";
|
|
4
4
|
export declare const knowherePluginConfigSchema: JsonSchemaObject;
|
|
5
5
|
export declare function resolveKnowhereConfig(api: OpenClawPluginApi): ResolvedKnowhereConfig;
|
|
6
|
+
export declare const API_KEY_URL = "https://knowhereto.ai/api-keys";
|
|
7
|
+
export declare const PURCHASE_CREDITS_URL = "https://knowhereto.ai/usage?buy=true";
|
|
6
8
|
export declare function assertKnowhereApiKey(config: ResolvedKnowhereConfig): void;
|
|
9
|
+
export declare function isPaymentRequiredError(error: unknown): boolean;
|
|
10
|
+
export declare function formatPaymentRequiredMessage(): string;
|
|
11
|
+
export declare function readPersistedApiKey(storageDir: string): Promise<string | null>;
|
|
12
|
+
export declare function persistApiKey(storageDir: string, apiKey: string): Promise<void>;
|