@wecode-ai/weibo-openclaw-plugin 1.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.md +59 -0
- package/index.ts +67 -0
- package/package.json +51 -0
- package/src/accounts.ts +134 -0
- package/src/bot.ts +486 -0
- package/src/channel.ts +391 -0
- package/src/client.ts +435 -0
- package/src/config-schema.ts +58 -0
- package/src/fingerprint.ts +25 -0
- package/src/monitor.ts +206 -0
- package/src/outbound-stream.ts +241 -0
- package/src/outbound.ts +49 -0
- package/src/plugin-sdk-compat.ts +82 -0
- package/src/policy.ts +10 -0
- package/src/runtime.ts +14 -0
- package/src/search-schema.ts +7 -0
- package/src/send.ts +80 -0
- package/src/sim-page.ts +140 -0
- package/src/sim-store.ts +186 -0
- package/src/targets.ts +14 -0
- package/src/token.ts +207 -0
- package/src/tools-config.ts +55 -0
- package/src/types.ts +95 -0
- package/src/weibo-hot-search.ts +345 -0
- package/src/weibo-search.ts +333 -0
- package/src/weibo-status.ts +341 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# openclaw-weibo
|
|
2
|
+
|
|
3
|
+
OpenClaw Weibo DM channel plugin - 微博私信通道插件
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## 使用
|
|
8
|
+
### 获取凭证
|
|
9
|
+
|
|
10
|
+
1. 打开微博客户端,私信 [@微博龙虾助手](https://weibo.com/u/6808810981)
|
|
11
|
+
2. 发送消息:`连接龙虾`
|
|
12
|
+
3. 收到回复示例:
|
|
13
|
+
```
|
|
14
|
+
您的应用凭证信息如下:
|
|
15
|
+
|
|
16
|
+
AppId: your-app-id
|
|
17
|
+
AppSecret: your-app-secret
|
|
18
|
+
|
|
19
|
+
如需重置凭证,请发送 "重置凭证" 命令。
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 配置OpenClaw
|
|
23
|
+
#### 安装插件
|
|
24
|
+
```
|
|
25
|
+
git clone https://gitee.com/wecode-ai/openclaw-weibo.git
|
|
26
|
+
cd openclaw-weibo
|
|
27
|
+
openclaw plugins install .
|
|
28
|
+
openclaw gateway restart
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
#### 配置凭证
|
|
32
|
+
使用命令配置:
|
|
33
|
+
```bash
|
|
34
|
+
openclaw config set 'channels.weibo.appSecret' 'your-appSecret'
|
|
35
|
+
openclaw config set 'channels.weibo.appId' 'your-appId'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
或编辑 `~/.openclaw/openclaw.config.json`:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
...existing config...
|
|
43
|
+
"channels": {
|
|
44
|
+
...existing config...
|
|
45
|
+
"weibo": {
|
|
46
|
+
"appId": "your-app-id",
|
|
47
|
+
"appSecret": "your-app-secret"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 插件网络访问说明
|
|
54
|
+
* 插件通过域名 `open-im.api.weibo.com` 来调用微博接口。
|
|
55
|
+
* 如在出入口网络受限环境下使用该插件请注意配置访问权限。
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { weiboPlugin } from "./src/channel.js";
|
|
3
|
+
import { setWeiboRuntime } from "./src/runtime.js";
|
|
4
|
+
import { reconnectWeiboMonitor } from "./src/monitor.js";
|
|
5
|
+
import { clearClientCache } from "./src/client.js";
|
|
6
|
+
import { clearTokenCache } from "./src/token.js";
|
|
7
|
+
import { registerWeiboSearchTools } from "./src/weibo-search.js";
|
|
8
|
+
import { registerWeiboStatusTools } from "./src/weibo-status.js";
|
|
9
|
+
import { registerWeiboHotSearchTools } from "./src/weibo-hot-search.js";
|
|
10
|
+
|
|
11
|
+
export { monitorWeiboProvider } from "./src/monitor.js";
|
|
12
|
+
export { sendMessageWeibo } from "./src/send.js";
|
|
13
|
+
export { weiboPlugin } from "./src/channel.js";
|
|
14
|
+
|
|
15
|
+
const plugin = {
|
|
16
|
+
id: "weibo",
|
|
17
|
+
name: "Weibo",
|
|
18
|
+
description: "Weibo DM channel plugin",
|
|
19
|
+
configSchema: { type: "object" as const, properties: {} },
|
|
20
|
+
register(api: OpenClawPluginApi) {
|
|
21
|
+
setWeiboRuntime(api.runtime);
|
|
22
|
+
api.registerChannel({ plugin: weiboPlugin });
|
|
23
|
+
registerWeiboSearchTools(api);
|
|
24
|
+
registerWeiboStatusTools(api);
|
|
25
|
+
registerWeiboHotSearchTools(api);
|
|
26
|
+
|
|
27
|
+
// 工具调用钩子
|
|
28
|
+
api.on("before_tool_call", (event) => {
|
|
29
|
+
if (event.toolName.startsWith("weibo_")) {
|
|
30
|
+
console.log(`[微博工具调用] ${event.toolName} 参数: ${JSON.stringify(event.params)}`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
api.on("after_tool_call", (event) => {
|
|
34
|
+
if (event.toolName.startsWith("weibo_")) {
|
|
35
|
+
if (event.error) {
|
|
36
|
+
console.error(`[微博工具调用失败] ${event.toolName} 错误: ${event.error}`);
|
|
37
|
+
} else {
|
|
38
|
+
console.log(`[微博工具调用成功] ${event.toolName} 耗时: ${event.durationMs}ms`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
api.registerGatewayMethod("weibo.reconnect", async ({ params, respond, context }) => {
|
|
44
|
+
const accountId =
|
|
45
|
+
typeof params.accountId === "string" && params.accountId.trim()
|
|
46
|
+
? params.accountId.trim()
|
|
47
|
+
: undefined;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
await reconnectWeiboMonitor(accountId);
|
|
51
|
+
clearClientCache(accountId);
|
|
52
|
+
clearTokenCache(accountId);
|
|
53
|
+
await context.stopChannel("weibo", accountId);
|
|
54
|
+
await context.startChannel("weibo", accountId);
|
|
55
|
+
respond(true, { ok: true, accountId: accountId ?? "default" });
|
|
56
|
+
} catch (err) {
|
|
57
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
58
|
+
respond(false, undefined, {
|
|
59
|
+
code: "weibo_reconnect_failed",
|
|
60
|
+
message,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wecode-ai/weibo-openclaw-plugin",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OpenClaw Weibo DM channel plugin",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test:unit": "vitest run",
|
|
8
|
+
"ci:check": "npx tsc --noEmit && npm run test:unit",
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"dev": "tsx",
|
|
11
|
+
"sim:server": "tsx weibo-server.ts"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"files": [
|
|
15
|
+
"index.ts",
|
|
16
|
+
"src/**/*.ts",
|
|
17
|
+
"!src/**/__tests__/**",
|
|
18
|
+
"!src/**/*.test.ts"
|
|
19
|
+
],
|
|
20
|
+
"openclaw": {
|
|
21
|
+
"extensions": [
|
|
22
|
+
"./index.ts"
|
|
23
|
+
],
|
|
24
|
+
"channel": {
|
|
25
|
+
"id": "weibo",
|
|
26
|
+
"label": "Weibo",
|
|
27
|
+
"selectionLabel": "Weibo (微博)",
|
|
28
|
+
"docsPath": "/channels/weibo",
|
|
29
|
+
"docsLabel": "weibo",
|
|
30
|
+
"blurb": "Weibo direct message channel.",
|
|
31
|
+
"order": 80
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@sinclair/typebox": "0.34.48",
|
|
36
|
+
"ws": "^8.18.0",
|
|
37
|
+
"zod": "^4.3.6"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^25.0.10",
|
|
41
|
+
"@types/ws": "^8.5.14",
|
|
42
|
+
"@vitest/coverage-v8": "^2.1.8",
|
|
43
|
+
"openclaw": "^2026.3.1",
|
|
44
|
+
"tsx": "^4.21.0",
|
|
45
|
+
"typescript": "^5.7.0",
|
|
46
|
+
"vitest": "^2.1.8"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"openclaw": ">=2026.3.1"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/accounts.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { WeiboConfig, ResolvedWeiboAccount } from "./types.js";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
5
|
+
const DEFAULT_WS_ENDPOINT = "ws://open-im.api.weibo.com/ws/stream";
|
|
6
|
+
const DEFAULT_TOKEN_ENDPOINT = "http://open-im.api.weibo.com/open/auth/ws_token";
|
|
7
|
+
|
|
8
|
+
function readOptionalNonBlankString(value: unknown): string | undefined {
|
|
9
|
+
if (typeof value === "number" && !Number.isNaN(value)) {
|
|
10
|
+
return String(value);
|
|
11
|
+
}
|
|
12
|
+
if (typeof value !== "string") {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
return trimmed ? trimmed : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveWeiboAccount({
|
|
20
|
+
cfg,
|
|
21
|
+
accountId = DEFAULT_ACCOUNT_ID,
|
|
22
|
+
}: {
|
|
23
|
+
cfg: ClawdbotConfig;
|
|
24
|
+
accountId?: string;
|
|
25
|
+
}): ResolvedWeiboAccount {
|
|
26
|
+
const weiboCfg = cfg.channels?.weibo as WeiboConfig | undefined;
|
|
27
|
+
|
|
28
|
+
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
29
|
+
const topLevelAppId = readOptionalNonBlankString(weiboCfg?.appId);
|
|
30
|
+
const topLevelAppSecret = readOptionalNonBlankString(weiboCfg?.appSecret);
|
|
31
|
+
const topLevelWsEndpoint = readOptionalNonBlankString(weiboCfg?.wsEndpoint);
|
|
32
|
+
const topLevelTokenEndpoint = readOptionalNonBlankString(weiboCfg?.tokenEndpoint);
|
|
33
|
+
|
|
34
|
+
if (isDefault && weiboCfg) {
|
|
35
|
+
const hasCredentials = !!(topLevelAppId && topLevelAppSecret);
|
|
36
|
+
return {
|
|
37
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
38
|
+
enabled: weiboCfg.enabled ?? true,
|
|
39
|
+
configured: hasCredentials,
|
|
40
|
+
name: "Default",
|
|
41
|
+
appId: topLevelAppId,
|
|
42
|
+
appSecret: topLevelAppSecret,
|
|
43
|
+
wsEndpoint: topLevelWsEndpoint ?? DEFAULT_WS_ENDPOINT,
|
|
44
|
+
tokenEndpoint: topLevelTokenEndpoint ?? DEFAULT_TOKEN_ENDPOINT,
|
|
45
|
+
config: {
|
|
46
|
+
dmPolicy: weiboCfg.dmPolicy ?? "open",
|
|
47
|
+
allowFrom: weiboCfg.allowFrom ?? [],
|
|
48
|
+
tokenEndpoint: topLevelTokenEndpoint ?? DEFAULT_TOKEN_ENDPOINT,
|
|
49
|
+
wsEndpoint: topLevelWsEndpoint ?? DEFAULT_WS_ENDPOINT,
|
|
50
|
+
textChunkLimit: weiboCfg.textChunkLimit,
|
|
51
|
+
chunkMode: weiboCfg.chunkMode ?? "raw",
|
|
52
|
+
blockStreaming: weiboCfg.blockStreaming ?? true,
|
|
53
|
+
tools: weiboCfg.tools,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const accountCfg = weiboCfg?.accounts?.[accountId];
|
|
59
|
+
const topLevel = {
|
|
60
|
+
appId: topLevelAppId,
|
|
61
|
+
appSecret: topLevelAppSecret,
|
|
62
|
+
wsEndpoint: topLevelWsEndpoint,
|
|
63
|
+
tokenEndpoint: topLevelTokenEndpoint,
|
|
64
|
+
dmPolicy: weiboCfg?.dmPolicy,
|
|
65
|
+
allowFrom: weiboCfg?.allowFrom,
|
|
66
|
+
textChunkLimit: weiboCfg?.textChunkLimit,
|
|
67
|
+
chunkMode: weiboCfg?.chunkMode,
|
|
68
|
+
blockStreaming: weiboCfg?.blockStreaming,
|
|
69
|
+
tools: weiboCfg?.tools,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const merged = {
|
|
73
|
+
appId: readOptionalNonBlankString(accountCfg?.appId) ?? topLevel.appId,
|
|
74
|
+
appSecret: readOptionalNonBlankString(accountCfg?.appSecret) ?? topLevel.appSecret,
|
|
75
|
+
wsEndpoint:
|
|
76
|
+
readOptionalNonBlankString(accountCfg?.wsEndpoint)
|
|
77
|
+
?? topLevel.wsEndpoint
|
|
78
|
+
?? DEFAULT_WS_ENDPOINT,
|
|
79
|
+
tokenEndpoint:
|
|
80
|
+
readOptionalNonBlankString(accountCfg?.tokenEndpoint)
|
|
81
|
+
?? topLevel.tokenEndpoint
|
|
82
|
+
?? DEFAULT_TOKEN_ENDPOINT,
|
|
83
|
+
dmPolicy: accountCfg?.dmPolicy ?? topLevel.dmPolicy ?? "open",
|
|
84
|
+
allowFrom: accountCfg?.allowFrom ?? topLevel.allowFrom ?? [],
|
|
85
|
+
textChunkLimit: accountCfg?.textChunkLimit ?? topLevel.textChunkLimit,
|
|
86
|
+
chunkMode: accountCfg?.chunkMode ?? topLevel.chunkMode ?? "raw",
|
|
87
|
+
blockStreaming: accountCfg?.blockStreaming ?? topLevel.blockStreaming ?? true,
|
|
88
|
+
tools: accountCfg?.tools ?? topLevel.tools,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const hasCredentials = !!(merged.appId && merged.appSecret);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
accountId,
|
|
95
|
+
enabled: accountCfg?.enabled ?? weiboCfg?.enabled ?? true,
|
|
96
|
+
configured: hasCredentials,
|
|
97
|
+
name: accountCfg?.name,
|
|
98
|
+
appId: merged.appId,
|
|
99
|
+
appSecret: merged.appSecret,
|
|
100
|
+
wsEndpoint: merged.wsEndpoint,
|
|
101
|
+
tokenEndpoint: merged.tokenEndpoint,
|
|
102
|
+
config: {
|
|
103
|
+
dmPolicy: merged.dmPolicy,
|
|
104
|
+
allowFrom: merged.allowFrom,
|
|
105
|
+
tokenEndpoint: merged.tokenEndpoint,
|
|
106
|
+
wsEndpoint: merged.wsEndpoint,
|
|
107
|
+
textChunkLimit: merged.textChunkLimit,
|
|
108
|
+
chunkMode: merged.chunkMode,
|
|
109
|
+
blockStreaming: merged.blockStreaming,
|
|
110
|
+
tools: merged.tools,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function listWeiboAccountIds(cfg: ClawdbotConfig): string[] {
|
|
116
|
+
const weiboCfg = cfg.channels?.weibo as WeiboConfig | undefined;
|
|
117
|
+
const accounts = weiboCfg?.accounts;
|
|
118
|
+
const ids = [DEFAULT_ACCOUNT_ID];
|
|
119
|
+
if (accounts) {
|
|
120
|
+
ids.push(...Object.keys(accounts));
|
|
121
|
+
}
|
|
122
|
+
return ids;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function resolveDefaultWeiboAccountId(cfg: ClawdbotConfig): string {
|
|
126
|
+
return DEFAULT_ACCOUNT_ID;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function listEnabledWeiboAccounts(cfg: ClawdbotConfig): ResolvedWeiboAccount[] {
|
|
130
|
+
const ids = listWeiboAccountIds(cfg);
|
|
131
|
+
return ids
|
|
132
|
+
.map((id) => resolveWeiboAccount({ cfg, accountId: id }))
|
|
133
|
+
.filter((a) => a.enabled && a.configured);
|
|
134
|
+
}
|