-
Notifications
You must be signed in to change notification settings - Fork 85
feat: 新增 6 个钉钉服务包 — 多维表格/日历/文档/云盘/消息/待办 #478
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
wjh747770454-creator
wants to merge
8
commits into
chaitin:main
Choose a base branch
from
wjh747770454-creator:feat/dingtalk-services
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
020d2b2
feat: add state-manager service — agent memory, distillation, task tr…
53f75b8
feat: add DingTalk service packages — AITable, Calendar, Doc, Drive, …
f488a86
fix: 修复看板 Owner/销售字段反了 + SyncWeekly 三个 bug
8ebdb81
fix: Monk Scan 安全与并发修复 — 命令注入/路径遍历/竞态/数据丢失
17d0f7e
fix(security): shellEscape 防御非字符串输入,防止 RPC 参数缺失时服务崩溃
f20c548
fix(security): queryEvents 查询轮转文件 + doc-memory 原子写入
4fdb859
fix(security): MonkeyScan 审计修复 — 并发竞态/原子写入/错误静默吞没/轮转数据保护
2c70a71
fix(security): GetState/SetState 并发竞态 + aitable dwsPath shell 转义
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,220 @@ | ||
| #!/usr/bin/env node | ||
| /** | ||
| * DingTalk AI Table Service — OctoBus 服务包 | ||
| */ | ||
| import { defineService, runServiceMain } from '@chaitin-ai/octobus-sdk'; | ||
| import { execFile } from 'child_process'; | ||
| import { readFileSync } from 'fs'; | ||
| import { fileURLToPath } from 'url'; | ||
| import { dirname, join } from 'path'; | ||
|
|
||
| const __filename = fileURLToPath(import.meta.url); | ||
| const __dirname = dirname(__filename); | ||
|
|
||
| // 加载看板配置(从 kanban.json 读取,避免 js-yaml 依赖) | ||
| let kanbanConfig = null; | ||
| function loadKanbanConfig(configPath) { | ||
| if (kanbanConfig) return kanbanConfig; | ||
| try { | ||
| const resolved = configPath || join(__dirname, '..', 'config', 'kanban.json'); | ||
| const raw = readFileSync(resolved, 'utf-8'); | ||
| kanbanConfig = JSON.parse(raw); | ||
| } catch (err) { | ||
| console.error(`[aitable] Failed to load kanban.json: ${err.message}`); | ||
| kanbanConfig = {}; | ||
| } | ||
| return kanbanConfig; | ||
| } | ||
|
|
||
| function runDws(command, timeout = 60000) { | ||
| const dwsPath = process.env.DWS_PATH || 'dws'; | ||
| // 对 dwsPath 做 shell 转义,防止 DWS_PATH 含空格/特殊字符时 shell 误解析 | ||
| // (与 dws-runner.js 的 shellEscape 一致) | ||
| const escapedPath = `'${dwsPath.replace(/'/g, "'\\''")}'`; | ||
| return new Promise((resolve) => { | ||
| execFile('sh', ['-c', `${escapedPath} ${command} --yes --format json`], { timeout, maxBuffer: 10*1024*1024 }, | ||
| (error, stdout) => { | ||
| const raw = stdout.trim(); | ||
| let data = null; | ||
| try { data = JSON.parse(raw); } catch { data = raw; } | ||
| // dws 退出码 0 不代表业务成功,要看 data.status | ||
| const status = data && typeof data === 'object' ? String(data.status || '').toLowerCase() : ''; | ||
| const businessFailed = status === 'error' || status === 'failed'; | ||
| if (error && error.code !== 0) { | ||
| resolve({ success: false, data, error: (data && data.summary) || error.message }); | ||
| } else if (businessFailed) { | ||
| const errMsg = (data && data.error && (data.error.message || data.error)) || (data && data.summary) || 'dws business error'; | ||
| resolve({ success: false, data, error: typeof errMsg === 'object' ? JSON.stringify(errMsg) : String(errMsg) }); | ||
| } else { | ||
| resolve({ success: true, data, error: '' }); | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| const shellEscape = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'"; | ||
|
|
||
| const service = defineService({ | ||
| handlers: { | ||
| 'dingtalk.aitable.v1.AITableService/CreateRecord': async (ctx) => { | ||
| const { baseId, tableId, fields } = ctx.request; | ||
| // dws CLI expects --fields as array: [{"cells": {"fieldId": value, ...}}] | ||
| // Values can be strings, or JSON-stringified objects/arrays for complex fields | ||
| const cells = {}; | ||
| for (const [key, val] of Object.entries(fields || {})) { | ||
| // Try to parse JSON strings back to objects/arrays for complex fields | ||
| if (typeof val === 'string' && (val.startsWith('{') || val.startsWith('['))) { | ||
| try { cells[key] = JSON.parse(val); } catch { cells[key] = val; } | ||
| } else { | ||
| cells[key] = val; | ||
| } | ||
| } | ||
| const recordsArray = JSON.stringify([{ cells }]); | ||
| const cmd = `aitable record create --base-id ${shellEscape(baseId)} --table-id ${shellEscape(tableId)} --fields ${shellEscape(recordsArray)}`; | ||
|
|
||
| const res = await runDws(cmd); | ||
| if (!res.success) return { success: false, recordId: '', error: res.error }; | ||
|
|
||
| // dws 把结果包在 data.data 里;newRecordIds 是新建记录 ID 列表 | ||
| const r = res.data?.data || res.data?.result || res.data; | ||
| const recordId = (Array.isArray(r?.newRecordIds) && r.newRecordIds[0]) | ||
| || (Array.isArray(r) && r[0]?.recordId) | ||
| || r?.recordId || ''; | ||
| return { success: true, recordId, error: '' }; | ||
| }, | ||
|
|
||
| 'dingtalk.aitable.v1.AITableService/QueryRecords': async (ctx) => { | ||
| const { baseId, tableId, limit } = ctx.request; | ||
| const cmd = `aitable record list --base-id ${shellEscape(baseId)} --table-id ${shellEscape(tableId)} --limit ${shellEscape(String(parseInt(limit || 10, 10)))}`; | ||
|
|
||
| const res = await runDws(cmd); | ||
| if (!res.success) return { success: false, records: [], error: res.error }; | ||
|
|
||
| // dws 把结果包在 data.data.records;每条记录字段在 cells(不是 fields) | ||
| const rawRecords = res.data?.data?.records || res.data?.result?.records || res.data?.records || []; | ||
| const list = Array.isArray(rawRecords) ? rawRecords : []; | ||
| const records = list.map((r) => ({ | ||
| recordId: r.recordId || r.id || '', | ||
| fields: r.cells || r.fields || {}, | ||
| })); | ||
| return { success: true, records, error: '' }; | ||
| }, | ||
|
|
||
| /** | ||
| * 看板记录创建(业务封装) | ||
| * 读 projects.yaml 的 kanban 配置,自动映射字段 ID → 构造 fields → 调用 CreateRecord | ||
| */ | ||
| 'dingtalk.aitable.v1.AITableService/CreateKanbanRecord': async (ctx) => { | ||
| const { summary, category, customer, date, taskType, description, salesman, owner } = ctx.request; | ||
|
|
||
| // 1. 加载配置 | ||
| const kanban = loadKanbanConfig(process.env.KANBAN_CONFIG_PATH); | ||
| if (!kanban) { | ||
| return { success: false, recordId: '', error: 'projects.yaml 中缺少 kanban 配置' }; | ||
| } | ||
|
|
||
| // 2. 映射任务类型 | ||
| const taskTypes = kanban.task_types || {}; | ||
| let taskTypeOption = null; | ||
| // 先精确匹配 taskType,再回退到 category | ||
| const lookupKey = taskType || category; | ||
| if (taskTypes[lookupKey]) { | ||
| taskTypeOption = taskTypes[lookupKey]; | ||
| } else { | ||
| // 模糊匹配:key 包含 category 的关键词 | ||
| for (const [key, val] of Object.entries(taskTypes)) { | ||
| if (key.includes(lookupKey) || lookupKey.includes(key)) { | ||
| taskTypeOption = val; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| if (!taskTypeOption) { | ||
| // 最终回退:用 category 本身作为名称 | ||
| taskTypeOption = { id: '', name: lookupKey }; | ||
| } | ||
|
|
||
| // 3. 映射客户分类 | ||
| const customers = kanban.customers || {}; | ||
| let customerOption = null; | ||
| if (customers[customer]) { | ||
| customerOption = customers[customer]; | ||
| } else { | ||
| // 模糊匹配:客户名中是否包含已知的 key | ||
| for (const [key, val] of Object.entries(customers)) { | ||
| if (customer.includes(key) || key.includes(customer)) { | ||
| customerOption = val; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| if (!customerOption && customer) { | ||
| customerOption = { id: '', name: customer }; | ||
| } | ||
|
|
||
| // 4. 提取二级部门(客户名中"-"后的部分) | ||
| let department2 = ''; | ||
| if (customer && customer.includes('-')) { | ||
| department2 = customer.split('-').slice(1).join('-'); | ||
| } | ||
|
|
||
| // 5. 构造 fields | ||
| const fields = {}; | ||
| // 日期字段(两个日期列) | ||
| fields['5wlytmi'] = date || ''; | ||
| fields['5sxy4yq'] = date || ''; | ||
| // 任务类型 | ||
| fields['wiisqll'] = taskTypeOption; | ||
| // 任务描述 | ||
| fields['q3aj1jn'] = summary || ''; | ||
| // 销售(kkcfjj6 = 销售字段) | ||
| const salesUser = salesman || 'naiting.zang'; | ||
| fields['kkcfjj6'] = [{ userId: salesUser }]; | ||
| // Owner/执行人(6olqudb = Owner字段) | ||
| const ownerUser = owner || 'jianhua.wang'; | ||
| fields['6olqudb'] = [{ userId: ownerUser }]; | ||
| // 周报摘要/说明 | ||
| fields['x2ysb28'] = description || summary || ''; | ||
| // 客户分类 | ||
| if (customerOption) { | ||
| fields['dacnewp'] = customerOption; | ||
| } | ||
| // 二级部门 | ||
| if (department2) { | ||
| fields['cgrfhw7'] = department2; | ||
| } | ||
| // 状态(固定:已完成) | ||
| if (kanban.status_done) { | ||
| fields['v5j63kh'] = kanban.status_done; | ||
| } | ||
|
|
||
| // 6. 调用 CreateRecord | ||
| const cells = {}; | ||
| for (const [key, val] of Object.entries(fields)) { | ||
| if (typeof val === 'object') { | ||
| cells[key] = val; | ||
| } else { | ||
| // Try to parse JSON strings back to objects/arrays for complex fields | ||
| if (typeof val === 'string' && (val.startsWith('{') || val.startsWith('['))) { | ||
| try { cells[key] = JSON.parse(val); } catch { cells[key] = val; } | ||
| } else { | ||
| cells[key] = val; | ||
| } | ||
| } | ||
| } | ||
| const recordsArray = JSON.stringify([{ cells }]); | ||
| const cmd = `aitable record create --base-id ${shellEscape(kanban.base_id)} --table-id ${shellEscape(kanban.table_id)} --fields ${shellEscape(recordsArray)}`; | ||
|
|
||
| const res = await runDws(cmd); | ||
| if (!res.success) return { success: false, recordId: '', error: res.error }; | ||
|
|
||
| const r = res.data?.data || res.data?.result || res.data; | ||
| const recordId = (Array.isArray(r?.newRecordIds) && r.newRecordIds[0]) | ||
| || (Array.isArray(r) && r[0]?.recordId) | ||
| || r?.recordId || ''; | ||
| return { success: true, recordId, error: '' }; | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| runServiceMain(service); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "type": "object", | ||
| "properties": { | ||
| "dwsPath": { | ||
| "type": "string", | ||
| "description": "dws CLI path", | ||
| "default": "dws" | ||
| }, | ||
| "kanbanConfigPath": { | ||
| "type": "string", | ||
| "description": "看板配置文件路径(kanban.json),用于字段映射", | ||
| "default": "./config/kanban.json" | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| { | ||
| "_README": "Kanban config template — replace with your own AITable base/table ID and field option IDs", | ||
| "base_id": "YOUR_AITABLE_BASE_ID", | ||
| "table_id": "YOUR_AITABLE_TABLE_ID", | ||
| "fields": { | ||
| "date_start": "5wlytmi", | ||
| "date_end": "5sxy4yq", | ||
| "task_type": "wiisqll", | ||
| "description": "q3aj1jn", | ||
| "owner": "kkcfjj6", | ||
| "salesman": "6olqudb", | ||
| "weekly_summary": "x2ysb28", | ||
| "customer_tier": "dacnewp", | ||
| "sub_department": "cgrfhw7", | ||
| "status": "v5j63kh" | ||
| }, | ||
| "task_types": { | ||
| "communication": { "id": "OPTION_ID", "name": "Communication" }, | ||
| "documentation": { "id": "OPTION_ID", "name": "Documentation" }, | ||
| "bidding": { "id": "OPTION_ID", "name": "Bidding" }, | ||
| "poc": { "id": "OPTION_ID", "name": "POC" } | ||
| }, | ||
| "customers": { | ||
| "customer_a": { "id": "OPTION_ID", "name": "Customer A Name" }, | ||
| "customer_b": { "id": "OPTION_ID", "name": "Customer B Name" } | ||
| }, | ||
| "status_done": { "id": "OPTION_ID", "name": "Done" } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "name": "@office-agent/dingtalk-aitable", | ||
| "displayName": "DingTalk AITable", | ||
| "version": "0.1.0", | ||
| "description": "DingTalk AITable — record CRUD, kanban operations, field management", | ||
| "type": "module", | ||
| "main": "bin/service.js", | ||
| "bin": "bin/service.js", | ||
| "scripts": { | ||
| "start": "node bin/service.js" | ||
| }, | ||
| "dependencies": { | ||
| "@chaitin-ai/octobus-sdk": "^0.1.0" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| syntax = "proto3"; | ||
| package dingtalk.aitable.v1; | ||
|
|
||
| service AITableService { | ||
| rpc CreateRecord(CreateRecordRequest) returns (CreateRecordResponse); | ||
| rpc QueryRecords(QueryRecordsRequest) returns (QueryRecordsResponse); | ||
| rpc CreateKanbanRecord(CreateKanbanRecordRequest) returns (CreateKanbanRecordResponse); | ||
| } | ||
|
|
||
| message CreateRecordRequest { | ||
| string base_id = 1; // AI Table base ID | ||
| string table_id = 2; // Table ID | ||
| map<string, string> fields = 3; // 字段名 → 值 | ||
| } | ||
|
|
||
| message CreateRecordResponse { | ||
| bool success = 1; | ||
| string record_id = 2; | ||
| string error = 3; | ||
| } | ||
|
|
||
| message QueryRecordsRequest { | ||
| string base_id = 1; | ||
| string table_id = 2; | ||
| string filter = 3; // 筛选条件(可选) | ||
| int32 limit = 4; | ||
| } | ||
|
|
||
| message AITableRecord { | ||
| string record_id = 1; | ||
| map<string, string> fields = 2; | ||
| } | ||
|
|
||
| message QueryRecordsResponse { | ||
| bool success = 1; | ||
| repeated AITableRecord records = 2; | ||
| string error = 3; | ||
| } | ||
|
|
||
| // 看板记录创建(业务封装,自动处理字段映射) | ||
| message CreateKanbanRecordRequest { | ||
| string summary = 1; // 任务描述,如"【交流】与XX沟通XX项目" | ||
| string category = 2; // 任务类型:沟通交流/文档材料/投标谈判/POC及其他 | ||
| string customer = 3; // 一级客户分类 | ||
| string date = 4; // 日期 YYYY-MM-DD | ||
| string task_type = 5; // 具体任务类型名(对应 projects.yaml 中 task_types 的 key) | ||
| string description = 6; // 必要说明 | ||
| string salesman = 7; // 销售 userId(从 user-config 读取,不设硬编码默认值) | ||
| string owner = 8; // 任务 Owner userId | ||
| } | ||
|
|
||
| message CreateKanbanRecordResponse { | ||
| bool success = 1; | ||
| string record_id = 2; | ||
| string error = 3; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "type": "object", | ||
| "properties": {} | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "schema": "chaitin.octobus.service.v1", | ||
| "name": "dingtalk__aitable", | ||
| "displayName": "DingTalk AITable Service", | ||
| "description": "DingTalk AITable integration — record creation, querying, and kanban board writing via dws CLI.", | ||
| "proto": { | ||
| "roots": ["proto"], | ||
| "files": ["proto/aitable.proto"] | ||
| }, | ||
| "configSchema": "config.schema.json", | ||
| "secretSchema": "secret.schema.json" | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.