@xuanyue202/qqbot 2026.3.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.
- package/dist/index.d.ts +1345 -0
- package/dist/index.js +13098 -0
- package/dist/index.js.map +1 -0
- package/openclaw.plugin.json +137 -0
- package/package.json +113 -0
- package/skills/qqbot-contact-send/SKILL.md +112 -0
- package/skills/qqbot-contact-send/scripts/prepare_send.py +141 -0
- package/skills/qqbot-contact-send/scripts/resolve_known_target.py +74 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "qqbot",
|
|
3
|
+
"name": "QQ Bot",
|
|
4
|
+
"description": "QQ 开放平台机器人消息渠道插件",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"channels": ["qqbot"],
|
|
7
|
+
"skills": ["./skills"],
|
|
8
|
+
"configSchema": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"additionalProperties": false,
|
|
11
|
+
"properties": {
|
|
12
|
+
"enabled": { "type": "boolean" },
|
|
13
|
+
"name": { "type": "string" },
|
|
14
|
+
"defaultAccount": { "type": "string" },
|
|
15
|
+
"appId": { "type": ["string", "number"] },
|
|
16
|
+
"clientSecret": { "type": "string" },
|
|
17
|
+
"displayAliases": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"additionalProperties": { "type": "string" }
|
|
20
|
+
},
|
|
21
|
+
"asr": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"properties": {
|
|
25
|
+
"enabled": { "type": "boolean" },
|
|
26
|
+
"appId": { "type": ["string", "number"] },
|
|
27
|
+
"secretId": { "type": "string" },
|
|
28
|
+
"secretKey": { "type": "string" }
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"markdownSupport": { "type": "boolean" },
|
|
32
|
+
"c2cMarkdownDeliveryMode": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"enum": ["passive", "proactive-table-only", "proactive-all"]
|
|
35
|
+
},
|
|
36
|
+
"c2cMarkdownChunkStrategy": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"enum": ["markdown-block", "length"]
|
|
39
|
+
},
|
|
40
|
+
"typingHeartbeatMode": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"enum": ["none", "idle", "always"]
|
|
43
|
+
},
|
|
44
|
+
"typingHeartbeatIntervalMs": { "type": "integer", "minimum": 1 },
|
|
45
|
+
"typingInputSeconds": { "type": "integer", "minimum": 1 },
|
|
46
|
+
"dmPolicy": { "type": "string", "enum": ["open", "pairing", "allowlist"] },
|
|
47
|
+
"groupPolicy": { "type": "string", "enum": ["open", "allowlist", "disabled"] },
|
|
48
|
+
"requireMention": { "type": "boolean" },
|
|
49
|
+
"allowFrom": { "type": "array", "items": { "type": "string" } },
|
|
50
|
+
"groupAllowFrom": { "type": "array", "items": { "type": "string" } },
|
|
51
|
+
"historyLimit": { "type": "integer", "minimum": 0 },
|
|
52
|
+
"textChunkLimit": { "type": "integer", "minimum": 1 },
|
|
53
|
+
"replyFinalOnly": { "type": "boolean" },
|
|
54
|
+
"longTaskNoticeDelayMs": { "type": "integer", "minimum": 0 },
|
|
55
|
+
"maxFileSizeMB": { "type": "number", "exclusiveMinimum": 0 },
|
|
56
|
+
"mediaTimeoutMs": { "type": "integer", "minimum": 1 },
|
|
57
|
+
"autoSendLocalPathMedia": { "type": "boolean" },
|
|
58
|
+
"inboundMedia": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"additionalProperties": false,
|
|
61
|
+
"properties": {
|
|
62
|
+
"dir": { "type": "string" },
|
|
63
|
+
"keepDays": { "type": "number", "minimum": 0 }
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"accounts": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"additionalProperties": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"additionalProperties": false,
|
|
71
|
+
"properties": {
|
|
72
|
+
"name": { "type": "string" },
|
|
73
|
+
"enabled": { "type": "boolean" },
|
|
74
|
+
"appId": { "type": ["string", "number"] },
|
|
75
|
+
"clientSecret": { "type": "string" },
|
|
76
|
+
"displayAliases": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"additionalProperties": { "type": "string" }
|
|
79
|
+
},
|
|
80
|
+
"asr": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"additionalProperties": false,
|
|
83
|
+
"properties": {
|
|
84
|
+
"enabled": { "type": "boolean" },
|
|
85
|
+
"appId": { "type": ["string", "number"] },
|
|
86
|
+
"secretId": { "type": "string" },
|
|
87
|
+
"secretKey": { "type": "string" }
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"markdownSupport": { "type": "boolean" },
|
|
91
|
+
"c2cMarkdownDeliveryMode": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"enum": ["passive", "proactive-table-only", "proactive-all"]
|
|
94
|
+
},
|
|
95
|
+
"c2cMarkdownChunkStrategy": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"enum": ["markdown-block", "length"]
|
|
98
|
+
},
|
|
99
|
+
"typingHeartbeatMode": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"enum": ["none", "idle", "always"]
|
|
102
|
+
},
|
|
103
|
+
"typingHeartbeatIntervalMs": { "type": "integer", "minimum": 1 },
|
|
104
|
+
"typingInputSeconds": { "type": "integer", "minimum": 1 },
|
|
105
|
+
"dmPolicy": { "type": "string", "enum": ["open", "pairing", "allowlist"] },
|
|
106
|
+
"groupPolicy": { "type": "string", "enum": ["open", "allowlist", "disabled"] },
|
|
107
|
+
"requireMention": { "type": "boolean" },
|
|
108
|
+
"allowFrom": { "type": "array", "items": { "type": "string" } },
|
|
109
|
+
"groupAllowFrom": { "type": "array", "items": { "type": "string" } },
|
|
110
|
+
"historyLimit": { "type": "integer", "minimum": 0 },
|
|
111
|
+
"textChunkLimit": { "type": "integer", "minimum": 1 },
|
|
112
|
+
"replyFinalOnly": { "type": "boolean" },
|
|
113
|
+
"longTaskNoticeDelayMs": { "type": "integer", "minimum": 0 },
|
|
114
|
+
"maxFileSizeMB": { "type": "number", "exclusiveMinimum": 0 },
|
|
115
|
+
"mediaTimeoutMs": { "type": "integer", "minimum": 1 },
|
|
116
|
+
"autoSendLocalPathMedia": { "type": "boolean" },
|
|
117
|
+
"inboundMedia": {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"additionalProperties": false,
|
|
120
|
+
"properties": {
|
|
121
|
+
"dir": { "type": "string" },
|
|
122
|
+
"keepDays": { "type": "number", "minimum": 0 }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
"uiHints": {
|
|
131
|
+
"appId": { "label": "App ID" },
|
|
132
|
+
"clientSecret": { "label": "Client Secret", "sensitive": true },
|
|
133
|
+
"asr.appId": { "label": "ASR App ID" },
|
|
134
|
+
"asr.secretId": { "label": "ASR Secret ID", "sensitive": true },
|
|
135
|
+
"asr.secretKey": { "label": "ASR Secret Key", "sensitive": true }
|
|
136
|
+
}
|
|
137
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xuanyue202/qqbot",
|
|
3
|
+
"version": "2026.3.21",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Moltbot QQ Bot channel plugin",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"openclaw.plugin.json",
|
|
10
|
+
"skills"
|
|
11
|
+
],
|
|
12
|
+
"openclaw": {
|
|
13
|
+
"extensions": [
|
|
14
|
+
"./dist/index.js"
|
|
15
|
+
],
|
|
16
|
+
"channel": {
|
|
17
|
+
"id": "qqbot",
|
|
18
|
+
"label": "QQ Bot",
|
|
19
|
+
"selectionLabel": "QQ Bot",
|
|
20
|
+
"docsPath": "/channels/qqbot",
|
|
21
|
+
"blurb": "QQ 开放平台机器人消息",
|
|
22
|
+
"aliases": [
|
|
23
|
+
"qq"
|
|
24
|
+
],
|
|
25
|
+
"order": 72
|
|
26
|
+
},
|
|
27
|
+
"install": {
|
|
28
|
+
"npmSpec": "@xuanyue202/qqbot",
|
|
29
|
+
"localPath": ".",
|
|
30
|
+
"defaultChoice": "npm"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"moltbot": {
|
|
34
|
+
"extensions": [
|
|
35
|
+
"./dist/index.js"
|
|
36
|
+
],
|
|
37
|
+
"channel": {
|
|
38
|
+
"id": "qqbot",
|
|
39
|
+
"label": "QQ Bot",
|
|
40
|
+
"selectionLabel": "QQ Bot",
|
|
41
|
+
"docsPath": "/channels/qqbot",
|
|
42
|
+
"blurb": "QQ 开放平台机器人消息",
|
|
43
|
+
"aliases": [
|
|
44
|
+
"qq"
|
|
45
|
+
],
|
|
46
|
+
"order": 72
|
|
47
|
+
},
|
|
48
|
+
"install": {
|
|
49
|
+
"npmSpec": "@xuanyue202/qqbot",
|
|
50
|
+
"localPath": ".",
|
|
51
|
+
"defaultChoice": "npm"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"clawdbot": {
|
|
55
|
+
"extensions": [
|
|
56
|
+
"./dist/index.js"
|
|
57
|
+
],
|
|
58
|
+
"channel": {
|
|
59
|
+
"id": "qqbot",
|
|
60
|
+
"label": "QQ Bot",
|
|
61
|
+
"selectionLabel": "QQ Bot",
|
|
62
|
+
"docsPath": "/channels/qqbot",
|
|
63
|
+
"blurb": "QQ 开放平台机器人消息",
|
|
64
|
+
"aliases": [
|
|
65
|
+
"qq"
|
|
66
|
+
],
|
|
67
|
+
"order": 72
|
|
68
|
+
},
|
|
69
|
+
"install": {
|
|
70
|
+
"npmSpec": "@xuanyue202/qqbot",
|
|
71
|
+
"localPath": ".",
|
|
72
|
+
"defaultChoice": "npm"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"main": "./dist/index.js",
|
|
76
|
+
"types": "./dist/index.d.ts",
|
|
77
|
+
"exports": {
|
|
78
|
+
".": {
|
|
79
|
+
"types": "./dist/index.d.ts",
|
|
80
|
+
"default": "./dist/index.js"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"scripts": {
|
|
84
|
+
"build": "tsup",
|
|
85
|
+
"dev": "tsup --watch",
|
|
86
|
+
"test": "vitest --run",
|
|
87
|
+
"test:watch": "vitest",
|
|
88
|
+
"release": "npm run build && npm publish"
|
|
89
|
+
},
|
|
90
|
+
"dependencies": {
|
|
91
|
+
"@xuanyue202/shared": "2026.3.21",
|
|
92
|
+
"ffmpeg-static": "^5.3.0",
|
|
93
|
+
"silk-wasm": "^3.7.1",
|
|
94
|
+
"ws": "^8.18.0"
|
|
95
|
+
},
|
|
96
|
+
"devDependencies": {
|
|
97
|
+
"@types/node": "^22.0.0",
|
|
98
|
+
"@types/ws": "^8.5.0",
|
|
99
|
+
"tsup": "^8.2.0",
|
|
100
|
+
"typescript": "^5.7.0",
|
|
101
|
+
"vitest": "^2.1.0",
|
|
102
|
+
"zod": "^3.23.0"
|
|
103
|
+
},
|
|
104
|
+
"peerDependencies": {
|
|
105
|
+
"moltbot": ">=0.1.0"
|
|
106
|
+
},
|
|
107
|
+
"peerDependenciesMeta": {
|
|
108
|
+
"moltbot": {
|
|
109
|
+
"optional": true
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"private": false
|
|
113
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qqbot-contact-send
|
|
3
|
+
description: Resolve a QQBot recipient from the local known-targets registry, distinguish between similarly named contacts, and send text or files to the intended QQ user with the correct target. Use whenever the user says things like “发给这个 QQ 联系人”, “把这个文件发给某个 QQ 用户”, “看看 known-targets.json 里是谁”, “确认发送对象”, or when you need to map a human-readable QQ contact name to a concrete `user:<openid>` target before sending. Prefer this skill over guessing from the current chat when multiple QQ users exist.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# qqbot-contact-send
|
|
7
|
+
|
|
8
|
+
Use this skill when the user wants to send something to a QQ contact identified by display name or nickname rather than by raw target id.
|
|
9
|
+
|
|
10
|
+
## What this skill is for
|
|
11
|
+
|
|
12
|
+
This skill helps avoid sending to the wrong QQ user when:
|
|
13
|
+
- the current chat user is not the intended recipient
|
|
14
|
+
- multiple QQ contacts exist in `known-targets.json`
|
|
15
|
+
- display names are similar or duplicated
|
|
16
|
+
- different users may use multiple agents with the same display name
|
|
17
|
+
- the user asks to inspect the local QQBot target registry before sending
|
|
18
|
+
|
|
19
|
+
## Data source
|
|
20
|
+
|
|
21
|
+
Read this file first when resolving a recipient:
|
|
22
|
+
|
|
23
|
+
- `~/.openclaw/qqbot/data/known-targets.json`
|
|
24
|
+
|
|
25
|
+
It contains entries like:
|
|
26
|
+
- `accountId`
|
|
27
|
+
- `kind`
|
|
28
|
+
- `target`
|
|
29
|
+
- `displayName`
|
|
30
|
+
- `lastSeenAt`
|
|
31
|
+
|
|
32
|
+
## Default workflow
|
|
33
|
+
|
|
34
|
+
1. Read `~/.openclaw/qqbot/data/known-targets.json`.
|
|
35
|
+
2. Default to the current session's `accountId` as the lookup scope unless the user explicitly asks to send across another agent/account.
|
|
36
|
+
3. Prefer the bundled helper script for deterministic lookup:
|
|
37
|
+
- `python3 {baseDir}/scripts/resolve_known_target.py "<name-or-target>" --account-id "<current-accountId>"`
|
|
38
|
+
4. Match the requested human name against `displayName` first.
|
|
39
|
+
5. If more than one candidate is plausible, show the candidates briefly and ask which one to use.
|
|
40
|
+
6. If exactly one match is clear, use that entry's `target` and `accountId`.
|
|
41
|
+
7. Optionally run the bundled send-prep helper:
|
|
42
|
+
- `python3 {baseDir}/scripts/prepare_send.py "<recipient>" --account-id "<current-accountId>" --file <path> [--caption <text>]`
|
|
43
|
+
- or `--text <message>`
|
|
44
|
+
8. Send via the `message` tool using:
|
|
45
|
+
- `channel: "qqbot"`
|
|
46
|
+
- `target: <resolved target>`
|
|
47
|
+
- `accountId: <resolved accountId>`
|
|
48
|
+
9. If sending a file, pass the local path directly.
|
|
49
|
+
10. If upload fails, report the real error plainly. Offer retry or text fallback if appropriate.
|
|
50
|
+
|
|
51
|
+
## Matching rules
|
|
52
|
+
|
|
53
|
+
Prefer in this order:
|
|
54
|
+
1. exact `displayName` match
|
|
55
|
+
2. exact user-provided target id if given
|
|
56
|
+
3. recent active contact with a highly similar name
|
|
57
|
+
|
|
58
|
+
Do not assume the current inbound sender is the destination when the user explicitly names another person.
|
|
59
|
+
|
|
60
|
+
## Ambiguity handling
|
|
61
|
+
|
|
62
|
+
Ask a short follow-up if:
|
|
63
|
+
- multiple entries share the same or similar name
|
|
64
|
+
- no clear `displayName` match exists
|
|
65
|
+
- the user says only “发给他/她” without context
|
|
66
|
+
|
|
67
|
+
Good follow-up style:
|
|
68
|
+
- `我查到两个候选:A(user:...)和 B(user:...),你要发给哪一个?`
|
|
69
|
+
|
|
70
|
+
## Output style
|
|
71
|
+
|
|
72
|
+
When reporting the resolved recipient, be brief and concrete:
|
|
73
|
+
- who it maps to
|
|
74
|
+
- which target will be used
|
|
75
|
+
- whether send succeeded or failed
|
|
76
|
+
|
|
77
|
+
## Important caveat
|
|
78
|
+
|
|
79
|
+
QQ file messages sent through the generic `message` tool may not automatically enter the qqbot plugin's `ref-index`, so later quote recovery can be incomplete. This does not block sending, but it may affect future reply-context restoration.
|
|
80
|
+
|
|
81
|
+
## Example prompts this skill should trigger on
|
|
82
|
+
|
|
83
|
+
- `把这个文件发给这个 QQ 联系人`
|
|
84
|
+
- `看 known-targets.json,确认发送对象`
|
|
85
|
+
- `发给这个备注名联系人,不是当前这个人`
|
|
86
|
+
- `把这份 md 发给另一个 QQ 用户`
|
|
87
|
+
- `先查联系人再发,别发错人`
|
|
88
|
+
|
|
89
|
+
## Helper scripts
|
|
90
|
+
|
|
91
|
+
Bundled helpers:
|
|
92
|
+
- `scripts/resolve_known_target.py` → resolve recipient candidates
|
|
93
|
+
- `scripts/prepare_send.py` → resolve recipient and generate a ready-to-use `message` tool payload
|
|
94
|
+
|
|
95
|
+
Examples:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
python3 {baseDir}/scripts/resolve_known_target.py "<contact-name>" --account-id "<current-accountId>"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
python3 {baseDir}/scripts/prepare_send.py "<contact-name>" --account-id "<current-accountId>" --file <path> --caption "<optional-caption>"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The scripts use:
|
|
106
|
+
- exact `displayName`
|
|
107
|
+
- exact `target`
|
|
108
|
+
- substring match
|
|
109
|
+
- `lastSeenAt` recency as tie-breaker
|
|
110
|
+
- current session `accountId` as the default scope filter
|
|
111
|
+
|
|
112
|
+
`prepare_send.py` does not send by itself; it emits a verified payload for the `message` tool so the agent can deliver without hand-building target fields.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
KNOWN_TARGETS = Path.home() / '.openclaw' / 'qqbot' / 'data' / 'known-targets.json'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def norm(s: str) -> str:
|
|
10
|
+
return (s or '').strip().casefold()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_targets(account_id: str | None = None):
|
|
14
|
+
if not KNOWN_TARGETS.exists():
|
|
15
|
+
return []
|
|
16
|
+
with KNOWN_TARGETS.open('r', encoding='utf-8') as f:
|
|
17
|
+
data = json.load(f)
|
|
18
|
+
items = data if isinstance(data, list) else []
|
|
19
|
+
if account_id:
|
|
20
|
+
account_norm = norm(account_id)
|
|
21
|
+
items = [item for item in items if norm(str(item.get('accountId', ''))) == account_norm]
|
|
22
|
+
return items
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def score(entry, query: str):
|
|
26
|
+
q = norm(query)
|
|
27
|
+
name = norm(entry.get('displayName', ''))
|
|
28
|
+
target = norm(entry.get('target', ''))
|
|
29
|
+
if not q:
|
|
30
|
+
return -1
|
|
31
|
+
if name == q:
|
|
32
|
+
return 300
|
|
33
|
+
if target == q:
|
|
34
|
+
return 290
|
|
35
|
+
if q in name and name:
|
|
36
|
+
return 200 - max(0, len(name) - len(q))
|
|
37
|
+
if q in target and target:
|
|
38
|
+
return 180 - max(0, len(target) - len(q))
|
|
39
|
+
return -1
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def ranked_matches(query: str, account_id: str | None = None):
|
|
43
|
+
items = load_targets(account_id)
|
|
44
|
+
ranked = []
|
|
45
|
+
for item in items:
|
|
46
|
+
s = score(item, query)
|
|
47
|
+
if s >= 0:
|
|
48
|
+
ranked.append((s, int(item.get('lastSeenAt', 0) or 0), item))
|
|
49
|
+
ranked.sort(key=lambda x: (x[0], x[1]), reverse=True)
|
|
50
|
+
out = []
|
|
51
|
+
for s, _, item in ranked:
|
|
52
|
+
out.append({
|
|
53
|
+
'score': s,
|
|
54
|
+
'displayName': item.get('displayName'),
|
|
55
|
+
'target': item.get('target'),
|
|
56
|
+
'accountId': item.get('accountId'),
|
|
57
|
+
'kind': item.get('kind'),
|
|
58
|
+
'lastSeenAt': item.get('lastSeenAt'),
|
|
59
|
+
'sourceChatType': item.get('sourceChatType'),
|
|
60
|
+
})
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def usage():
|
|
65
|
+
print('Usage:', file=sys.stderr)
|
|
66
|
+
print(' prepare_send.py <recipient> [--account-id <id>] --file <path> [--caption <text>]', file=sys.stderr)
|
|
67
|
+
print(' prepare_send.py <recipient> [--account-id <id>] --text <message>', file=sys.stderr)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main():
|
|
71
|
+
if len(sys.argv) < 4:
|
|
72
|
+
usage()
|
|
73
|
+
sys.exit(2)
|
|
74
|
+
|
|
75
|
+
recipient = sys.argv[1]
|
|
76
|
+
args = sys.argv[2:]
|
|
77
|
+
file_path = None
|
|
78
|
+
caption = None
|
|
79
|
+
text = None
|
|
80
|
+
account_id = None
|
|
81
|
+
|
|
82
|
+
i = 0
|
|
83
|
+
while i < len(args):
|
|
84
|
+
a = args[i]
|
|
85
|
+
if a == '--account-id' and i + 1 < len(args):
|
|
86
|
+
account_id = args[i + 1]
|
|
87
|
+
i += 2
|
|
88
|
+
elif a == '--file' and i + 1 < len(args):
|
|
89
|
+
file_path = args[i + 1]
|
|
90
|
+
i += 2
|
|
91
|
+
elif a == '--caption' and i + 1 < len(args):
|
|
92
|
+
caption = args[i + 1]
|
|
93
|
+
i += 2
|
|
94
|
+
elif a == '--text' and i + 1 < len(args):
|
|
95
|
+
text = args[i + 1]
|
|
96
|
+
i += 2
|
|
97
|
+
else:
|
|
98
|
+
usage()
|
|
99
|
+
sys.exit(2)
|
|
100
|
+
|
|
101
|
+
if bool(file_path) == bool(text):
|
|
102
|
+
print(json.dumps({'error': 'Provide exactly one of --file or --text'}, ensure_ascii=False, indent=2))
|
|
103
|
+
sys.exit(2)
|
|
104
|
+
|
|
105
|
+
matches = ranked_matches(recipient, account_id)
|
|
106
|
+
if not matches:
|
|
107
|
+
print(json.dumps({'status': 'no_match', 'recipient': recipient, 'matches': []}, ensure_ascii=False, indent=2))
|
|
108
|
+
sys.exit(1)
|
|
109
|
+
|
|
110
|
+
top = matches[0]
|
|
111
|
+
ambiguous = len(matches) > 1 and matches[1]['score'] == top['score']
|
|
112
|
+
|
|
113
|
+
payload = {
|
|
114
|
+
'channel': 'qqbot',
|
|
115
|
+
'target': top['target'],
|
|
116
|
+
'accountId': top['accountId'],
|
|
117
|
+
}
|
|
118
|
+
if file_path:
|
|
119
|
+
p = Path(file_path)
|
|
120
|
+
payload['path'] = str(p)
|
|
121
|
+
payload['pathExists'] = p.exists()
|
|
122
|
+
if caption:
|
|
123
|
+
payload['caption'] = caption
|
|
124
|
+
else:
|
|
125
|
+
payload['message'] = text
|
|
126
|
+
|
|
127
|
+
result = {
|
|
128
|
+
'status': 'ambiguous' if ambiguous else 'ok',
|
|
129
|
+
'recipient': recipient,
|
|
130
|
+
'accountScope': account_id,
|
|
131
|
+
'resolved': top,
|
|
132
|
+
'matches': matches[:5],
|
|
133
|
+
'messageToolPayload': payload,
|
|
134
|
+
'note': 'Use message(action=send, ...messageToolPayload) for actual delivery.'
|
|
135
|
+
}
|
|
136
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
137
|
+
sys.exit(3 if ambiguous else 0)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == '__main__':
|
|
141
|
+
main()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
KNOWN_TARGETS = Path.home() / ".openclaw" / "qqbot" / "data" / "known-targets.json"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def norm(s: str) -> str:
|
|
10
|
+
return (s or "").strip().casefold()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_targets(account_id: str | None = None):
|
|
14
|
+
if not KNOWN_TARGETS.exists():
|
|
15
|
+
return []
|
|
16
|
+
with KNOWN_TARGETS.open("r", encoding="utf-8") as f:
|
|
17
|
+
data = json.load(f)
|
|
18
|
+
items = data if isinstance(data, list) else []
|
|
19
|
+
if account_id:
|
|
20
|
+
account_norm = norm(account_id)
|
|
21
|
+
items = [item for item in items if norm(str(item.get("accountId", ""))) == account_norm]
|
|
22
|
+
return items
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def score(entry, query: str):
|
|
26
|
+
q = norm(query)
|
|
27
|
+
name = norm(entry.get("displayName", ""))
|
|
28
|
+
target = norm(entry.get("target", ""))
|
|
29
|
+
if not q:
|
|
30
|
+
return -1
|
|
31
|
+
if name == q:
|
|
32
|
+
return 300
|
|
33
|
+
if target == q:
|
|
34
|
+
return 290
|
|
35
|
+
if q in name and name:
|
|
36
|
+
return 200 - (len(name) - len(q))
|
|
37
|
+
if q in target and target:
|
|
38
|
+
return 180 - (len(target) - len(q))
|
|
39
|
+
return -1
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main():
|
|
43
|
+
if len(sys.argv) < 2:
|
|
44
|
+
print("Usage: resolve_known_target.py <query> [--account-id <id>]", file=sys.stderr)
|
|
45
|
+
sys.exit(2)
|
|
46
|
+
|
|
47
|
+
query = sys.argv[1]
|
|
48
|
+
account_id = None
|
|
49
|
+
if len(sys.argv) >= 4 and sys.argv[2] == "--account-id":
|
|
50
|
+
account_id = sys.argv[3]
|
|
51
|
+
items = load_targets(account_id)
|
|
52
|
+
ranked = []
|
|
53
|
+
for item in items:
|
|
54
|
+
s = score(item, query)
|
|
55
|
+
if s >= 0:
|
|
56
|
+
ranked.append((s, int(item.get("lastSeenAt", 0) or 0), item))
|
|
57
|
+
|
|
58
|
+
ranked.sort(key=lambda x: (x[0], x[1]), reverse=True)
|
|
59
|
+
out = []
|
|
60
|
+
for s, _, item in ranked:
|
|
61
|
+
out.append({
|
|
62
|
+
"score": s,
|
|
63
|
+
"displayName": item.get("displayName"),
|
|
64
|
+
"target": item.get("target"),
|
|
65
|
+
"accountId": item.get("accountId"),
|
|
66
|
+
"kind": item.get("kind"),
|
|
67
|
+
"lastSeenAt": item.get("lastSeenAt"),
|
|
68
|
+
"sourceChatType": item.get("sourceChatType"),
|
|
69
|
+
})
|
|
70
|
+
print(json.dumps(out, ensure_ascii=False, indent=2))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|