opencode-dingtalk 0.2.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.
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Instance registry — manages all BotInstance objects in the process.
3
+ *
4
+ * Provides a central place to create, look up, and destroy instances
5
+ * so that both plugin mode and standalone mode share the same bookkeeping.
6
+ */
7
+ import { BotInstance } from "./instance.js";
8
+ const instances = new Map();
9
+ // DEBUG: Track instance creation
10
+ function debugLog(...args) {
11
+ const fs = require("fs");
12
+ const logFile = "/tmp/opencode-dingtalk-debug.log";
13
+ const timestamp = new Date().toISOString();
14
+ const message = args
15
+ .map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a)))
16
+ .join(" ");
17
+ const line = `[${timestamp}] [REGISTRY] ${message}\n`;
18
+ try {
19
+ fs.appendFileSync(logFile, line);
20
+ }
21
+ catch { }
22
+ }
23
+ /**
24
+ * Create (and register) a new BotInstance.
25
+ * Throws if an instance with the same id already exists.
26
+ */
27
+ export function createInstance(options) {
28
+ debugLog("Creating NEW instance:", options.instanceId, "botConfig:", !!options.botConfig);
29
+ if (instances.has(options.instanceId)) {
30
+ throw new Error(`Instance "${options.instanceId}" already exists`);
31
+ }
32
+ const instance = new BotInstance(options);
33
+ instances.set(options.instanceId, instance);
34
+ debugLog("Instance created, total instances:", instances.size);
35
+ return instance;
36
+ }
37
+ /**
38
+ * Create or return an existing instance with the given id.
39
+ * If the instance already exists, updates its client / serverUrl if provided.
40
+ */
41
+ export function getOrCreateInstance(options) {
42
+ const existing = instances.get(options.instanceId);
43
+ if (existing) {
44
+ debugLog("Found EXISTING instance:", options.instanceId, "update botConfig:", !!options.botConfig);
45
+ if (options.client)
46
+ existing.state.client = options.client;
47
+ if (options.serverUrl)
48
+ existing.state.serverUrl = options.serverUrl;
49
+ if (options.botConfig)
50
+ existing.state.botConfig = options.botConfig;
51
+ debugLog("After update, existing.state.botConfig:", !!existing.state.botConfig);
52
+ return existing;
53
+ }
54
+ debugLog("Instance NOT found, will create new one");
55
+ return createInstance(options);
56
+ }
57
+ export function getInstance(id) {
58
+ return instances.get(id);
59
+ }
60
+ export function removeInstance(id) {
61
+ return instances.delete(id);
62
+ }
63
+ export function getAllInstances() {
64
+ return Array.from(instances.values());
65
+ }
66
+ /** Stop all running instances and clear the registry. */
67
+ export async function shutdownAll() {
68
+ const all = getAllInstances();
69
+ for (const inst of all) {
70
+ try {
71
+ if (inst.isRunning)
72
+ await inst.stop();
73
+ }
74
+ catch { }
75
+ }
76
+ instances.clear();
77
+ }
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ import { createOpencodeClient } from "@opencode-ai/sdk";
3
+ import { resolveInstances, getDefaultConfigPath, getConfigFilePath, } from "./config.js";
4
+ import { createInstance, shutdownAll } from "./registry.js";
5
+ import { logger } from "./logger.js";
6
+ // ============ Event Subscription ============
7
+ async function subscribeToEvents(label, client, instance) {
8
+ logger.log(`[opencode-dingtalk][${label}] Subscribing to OpenCode events...`);
9
+ try {
10
+ const result = await client.global.event({});
11
+ const stream = result.stream;
12
+ logger.log(`[opencode-dingtalk][${label}] Event subscription active`);
13
+ for await (const globalEvent of stream) {
14
+ if (globalEvent) {
15
+ try {
16
+ const payload = globalEvent.payload;
17
+ await instance.handleEvent(payload);
18
+ }
19
+ catch (err) {
20
+ logger.error(`[opencode-dingtalk][${label}] Error handling event:`, err);
21
+ }
22
+ }
23
+ }
24
+ }
25
+ catch (err) {
26
+ logger.error(`[opencode-dingtalk][${label}] Event subscription error:`, err);
27
+ setTimeout(() => subscribeToEvents(label, client, instance), 5000);
28
+ }
29
+ }
30
+ // ============ Main ============
31
+ async function main() {
32
+ const configs = resolveInstances();
33
+ // Filter out configs without credentials
34
+ const valid = configs.filter((c) => c.clientId && c.clientSecret);
35
+ if (valid.length === 0) {
36
+ const configPath = getDefaultConfigPath();
37
+ logger.error(`[opencode-dingtalk] Missing credentials. Configure in one of:\n` +
38
+ ` 1. Config file: ${configPath}\n` +
39
+ ` 2. Env vars: DINGTALK_CLIENT_ID / DINGTALK_CLIENT_SECRET`);
40
+ process.exit(1);
41
+ }
42
+ logger.log(`[opencode-dingtalk] Starting ${valid.length} instance(s) in standalone mode...`);
43
+ if (getConfigFilePath()) {
44
+ logger.log(`[opencode-dingtalk] Config loaded from ${getConfigFilePath()}`);
45
+ }
46
+ for (const config of valid) {
47
+ const label = config.name || config.clientId;
48
+ const instanceId = config.clientId;
49
+ logger.log(`[opencode-dingtalk][${label}] Connecting to OpenCode at ${config.opencodeUrl}`);
50
+ logger.log(`[opencode-dingtalk][${label}] Message type: ${config.messageType}, notify: ${config.notifyLevel}`);
51
+ try {
52
+ const client = createOpencodeClient({
53
+ baseUrl: config.opencodeUrl,
54
+ });
55
+ const instance = createInstance({
56
+ instanceId,
57
+ client,
58
+ serverUrl: config.opencodeUrl,
59
+ botConfig: {
60
+ clientId: config.clientId,
61
+ clientSecret: config.clientSecret,
62
+ robotCode: config.robotCode,
63
+ messageType: config.messageType,
64
+ cardTemplateId: config.cardTemplateId,
65
+ notifyLevel: config.notifyLevel,
66
+ },
67
+ });
68
+ // Resolve initial session
69
+ const sessionsRes = await client.session.list({});
70
+ if (sessionsRes.data && sessionsRes.data.length > 0) {
71
+ instance.state.activeSessionId = sessionsRes.data[0].id;
72
+ logger.log(`[opencode-dingtalk][${label}] Using session: ${instance.state.activeSessionId}`);
73
+ }
74
+ else {
75
+ const newSession = await client.session.create({});
76
+ if (newSession.data) {
77
+ instance.state.activeSessionId = newSession.data.id;
78
+ logger.log(`[opencode-dingtalk][${label}] Created new session: ${instance.state.activeSessionId}`);
79
+ }
80
+ }
81
+ subscribeToEvents(label, client, instance);
82
+ await instance.startWithConfig({
83
+ clientId: config.clientId,
84
+ clientSecret: config.clientSecret,
85
+ robotCode: config.robotCode,
86
+ messageType: config.messageType,
87
+ cardTemplateId: config.cardTemplateId,
88
+ notifyLevel: config.notifyLevel,
89
+ });
90
+ logger.log(`[opencode-dingtalk][${label}] Bot is running.`);
91
+ }
92
+ catch (err) {
93
+ logger.error(`[opencode-dingtalk][${label}] Failed to start:`, err);
94
+ }
95
+ }
96
+ logger.log("[opencode-dingtalk] All instances started. Press Ctrl+C to stop.");
97
+ // Keep the process alive
98
+ await new Promise(() => { });
99
+ }
100
+ main();
101
+ async function shutdown() {
102
+ logger.log("\n[opencode-dingtalk] Shutting down all instances...");
103
+ try {
104
+ await shutdownAll();
105
+ }
106
+ catch { }
107
+ process.exit(0);
108
+ }
109
+ process.on("SIGINT", shutdown);
110
+ process.on("SIGTERM", shutdown);