@longshine-aimanager/ai-manager-my-ai 0.1.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 ADDED
@@ -0,0 +1,63 @@
1
+ # ai-manager-my-ai MCP
2
+
3
+ AI Manager“我的 AI”个人只读 MCP。它查询当前 Key 所属用户及其“我的部门”授权范围,不提供写入、上报或管理能力。
4
+
5
+ ## 要求
6
+
7
+ - Node.js 18+
8
+ - AI Manager“我的 AI 授权 Key”
9
+
10
+ 登录 `https://token.longshine.com/ai-manage/employee/my-ai`,在右上角头像菜单中生成 Key。Key 只应写入 MCP 配置,不要粘贴到对话、日志或代码中。
11
+
12
+ ## Codex 配置
13
+
14
+ ```toml
15
+ [mcp_servers.ai-manager-my-ai]
16
+ command = "npx"
17
+ args = ["-y", "@longshine-aimanager/ai-manager-my-ai@latest"]
18
+
19
+ [mcp_servers.ai-manager-my-ai.env]
20
+ AI_MANAGER_MY_AI_KEY = "aimq_xxx"
21
+ ```
22
+
23
+ ## Claude Desktop 配置
24
+
25
+ ```json
26
+ {
27
+ "mcpServers": {
28
+ "ai-manager-my-ai": {
29
+ "command": "npx",
30
+ "args": ["-y", "@longshine-aimanager/ai-manager-my-ai@latest"],
31
+ "env": {
32
+ "AI_MANAGER_MY_AI_KEY": "aimq_xxx"
33
+ }
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ## 工具
40
+
41
+ | 工具 | 能力 |
42
+ | --- | --- |
43
+ | `status` | 检查 Key 是否已配置,不返回 Key。 |
44
+ | `get_my_ai_summary` | 查询个人费用、Token、交互及渠道汇总。 |
45
+ | `get_my_ai_details` | 查询个人完整日级明细。 |
46
+ | `get_my_ai_code_output` | 查询个人代码产出与代码活动量/元。 |
47
+ | `get_my_ai_autonomous_details` | 查询个人实时自主上报完整明细。 |
48
+ | `get_my_department_summary` | 查询授权部门费用、Token、交互及渠道汇总。 |
49
+ | `get_my_department_details` | 查询授权部门完整日级明细。 |
50
+ | `get_my_department_code_output` | 查询授权部门代码产出与代码活动量/元。 |
51
+
52
+ 默认查询上海时区最近 30 个自然日。自定义日期为闭区间,最长 90 天,结束日期不能晚于上海当天。明细工具会自动拉取完整分页;任一分页失败时整个工具调用失败。
53
+
54
+ MCP 固定连接 `https://token.longshine.com/ai-manage`。Key 被撤销、轮换、所属用户停用或部门权限被撤销后,下一次请求立即按最新状态生效。
55
+
56
+ ## 发布
57
+
58
+ ```bash
59
+ npm run publish:dry-run
60
+ npm run publish:npm
61
+ ```
62
+
63
+ 发布脚本默认使用 npm staged approval;新包首次发布需使用 `../../script/publish-ai-manager-my-ai-mcp-npm.sh --direct`。发布结果记录在本目录的 `PUBLISH_LOG.jsonl`。
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { startServer } from "../src/server.mjs";
3
+
4
+ startServer();
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@longshine-aimanager/ai-manager-my-ai",
3
+ "version": "0.1.0",
4
+ "description": "Read-only AI Manager My AI MCP server.",
5
+ "type": "module",
6
+ "bin": {
7
+ "ai-manager-my-ai": "bin/ai-manager-my-ai.mjs"
8
+ },
9
+ "files": ["bin/**/*.mjs", "src/**/*.mjs", "README.md"],
10
+ "scripts": {
11
+ "test": "node --test tests/*.test.mjs",
12
+ "pack:dry-run": "npm pack --dry-run",
13
+ "publish:dry-run": "../../script/publish-ai-manager-my-ai-mcp-npm.sh --dry-run",
14
+ "publish:npm": "../../script/publish-ai-manager-my-ai-mcp-npm.sh"
15
+ },
16
+ "engines": {"node": ">=18"},
17
+ "license": "UNLICENSED"
18
+ }
package/src/client.mjs ADDED
@@ -0,0 +1,126 @@
1
+ export const PRODUCTION_BASE_URL = "https://token.longshine.com/ai-manage";
2
+
3
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
4
+ const DAY_MS = 24 * 60 * 60 * 1000;
5
+
6
+ function parseDate(value, label) {
7
+ if (!ISO_DATE.test(value)) throw new Error(`${label} 必须为 YYYY-MM-DD`);
8
+ const parsed = new Date(`${value}T00:00:00.000Z`);
9
+ if (Number.isNaN(parsed.valueOf()) || parsed.toISOString().slice(0, 10) !== value) {
10
+ throw new Error(`${label} 日期无效`);
11
+ }
12
+ return parsed;
13
+ }
14
+
15
+ function shiftDate(value, days) {
16
+ const parsed = parseDate(value, "日期");
17
+ parsed.setUTCDate(parsed.getUTCDate() + days);
18
+ return parsed.toISOString().slice(0, 10);
19
+ }
20
+
21
+ export function shanghaiToday(now = new Date()) {
22
+ const parts = Object.fromEntries(
23
+ new Intl.DateTimeFormat("en-US", {
24
+ timeZone: "Asia/Shanghai",
25
+ year: "numeric",
26
+ month: "2-digit",
27
+ day: "2-digit",
28
+ })
29
+ .formatToParts(now)
30
+ .map(({ type, value }) => [type, value]),
31
+ );
32
+ return `${parts.year}-${parts.month}-${parts.day}`;
33
+ }
34
+
35
+ export function normalizeRange(args = {}, today = shanghaiToday()) {
36
+ if (Boolean(args.from) !== Boolean(args.to)) {
37
+ throw new Error("from 和 to 必须同时提供");
38
+ }
39
+ const from = args.from || shiftDate(today, -29);
40
+ const to = args.to || today;
41
+ const start = parseDate(from, "from");
42
+ const end = parseDate(to, "to");
43
+ const current = parseDate(today, "上海当天");
44
+ if (start > end) throw new Error("开始日期不能晚于结束日期");
45
+ if (end > current) throw new Error("结束日期不能晚于上海当天");
46
+ if ((end - start) / DAY_MS >= 90) {
47
+ throw new Error("查询范围不能超过 90 天");
48
+ }
49
+ return { range: "custom", from, to };
50
+ }
51
+
52
+ export function configuredKey(env = process.env) {
53
+ const key = String(env.AI_MANAGER_MY_AI_KEY || "").trim();
54
+ if (!key) throw new Error("未配置 AI_MANAGER_MY_AI_KEY");
55
+ if (!key.startsWith("aimq_")) throw new Error("AI_MANAGER_MY_AI_KEY 格式无效");
56
+ return key;
57
+ }
58
+
59
+ export function createMyAiClient({
60
+ key,
61
+ fetchImpl = globalThis.fetch,
62
+ baseUrl = PRODUCTION_BASE_URL,
63
+ }) {
64
+ return {
65
+ async get(path, params = {}) {
66
+ const url = new URL(`${baseUrl}${path}`);
67
+ for (const [name, value] of Object.entries(params)) {
68
+ if (value !== undefined && value !== null) {
69
+ url.searchParams.set(name, String(value));
70
+ }
71
+ }
72
+ const response = await fetchImpl(url.toString(), {
73
+ method: "GET",
74
+ headers: { Authorization: `Bearer ${key}`, Accept: "application/json" },
75
+ signal: AbortSignal.timeout(30_000),
76
+ });
77
+ if (!response.ok) throw await responseError(response, path);
78
+ try {
79
+ return await response.json();
80
+ } catch {
81
+ throw new Error(`接口 ${path} 返回非 JSON 响应`);
82
+ }
83
+ },
84
+ };
85
+ }
86
+
87
+ export async function allRows(fetchPage, path, params) {
88
+ const rows = [];
89
+ let page = 1;
90
+ let expectedTotal = null;
91
+ while (expectedTotal === null || rows.length < expectedTotal) {
92
+ const payload = await fetchPage(path, { ...params, page, page_size: 100 });
93
+ if (
94
+ !Array.isArray(payload?.rows) ||
95
+ !Number.isInteger(payload?.total) ||
96
+ payload.total < 0
97
+ ) {
98
+ throw new Error(`接口 ${path} 分页结构无效`);
99
+ }
100
+ if (expectedTotal !== null && payload.total !== expectedTotal) {
101
+ throw new Error(`接口 ${path} 分页总数发生变化`);
102
+ }
103
+ expectedTotal = payload.total;
104
+ if (payload.rows.length === 0 && rows.length < expectedTotal) {
105
+ throw new Error(`接口 ${path} 分页提前结束`);
106
+ }
107
+ rows.push(...payload.rows);
108
+ page += 1;
109
+ }
110
+ return rows;
111
+ }
112
+
113
+ async function responseError(response, path) {
114
+ if (response.status === 401) {
115
+ return new Error("授权 Key 无效、已撤销、已轮换或所属用户已停用");
116
+ }
117
+ if (response.status === 403) {
118
+ return new Error("授权 Key 无权访问该数据");
119
+ }
120
+ if (response.status === 429) {
121
+ return new Error(
122
+ `请求过于频繁,请在 ${response.headers.get("retry-after") || "稍后"} 秒后重试`,
123
+ );
124
+ }
125
+ return new Error(`接口 ${path} 返回 HTTP ${response.status}`);
126
+ }
package/src/server.mjs ADDED
@@ -0,0 +1,83 @@
1
+ import fs from "node:fs";
2
+ import readline from "node:readline";
3
+
4
+ import { createToolRuntime, tools } from "./tools.mjs";
5
+
6
+ const packageJson = JSON.parse(
7
+ fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"),
8
+ );
9
+
10
+ function toolResult(payload) {
11
+ return {
12
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
13
+ };
14
+ }
15
+
16
+ function toolError(error) {
17
+ return {
18
+ isError: true,
19
+ content: [
20
+ {
21
+ type: "text",
22
+ text: error instanceof Error ? error.message : String(error),
23
+ },
24
+ ],
25
+ };
26
+ }
27
+
28
+ export function startServer({
29
+ input = process.stdin,
30
+ output = process.stdout,
31
+ env = process.env,
32
+ } = {}) {
33
+ const runtime = createToolRuntime({
34
+ key: String(env.AI_MANAGER_MY_AI_KEY || "").trim(),
35
+ });
36
+ const write = (value) => output.write(`${JSON.stringify(value)}\n`);
37
+ const result = (id, value) => write({ jsonrpc: "2.0", id, result: value });
38
+ const fail = (id, code, message) =>
39
+ write({ jsonrpc: "2.0", id, error: { code, message } });
40
+ const lines = readline.createInterface({ input, terminal: false });
41
+
42
+ lines.on("line", async (line) => {
43
+ if (!line.trim()) return;
44
+ let request;
45
+ try {
46
+ request = JSON.parse(line);
47
+ } catch {
48
+ fail(null, -32700, "Parse error");
49
+ return;
50
+ }
51
+ const { id, method, params } = request;
52
+ if (method === "notifications/initialized") return;
53
+ if (method === "initialize") {
54
+ result(id, {
55
+ protocolVersion: "2024-11-05",
56
+ capabilities: { tools: {} },
57
+ serverInfo: {
58
+ name: packageJson.name,
59
+ version: packageJson.version,
60
+ },
61
+ });
62
+ return;
63
+ }
64
+ if (method === "tools/list") {
65
+ result(id, { tools });
66
+ return;
67
+ }
68
+ if (method === "tools/call") {
69
+ try {
70
+ result(
71
+ id,
72
+ toolResult(await runtime.call(params?.name, params?.arguments || {})),
73
+ );
74
+ } catch (error) {
75
+ result(id, toolError(error));
76
+ }
77
+ return;
78
+ }
79
+ fail(id, -32601, `Method not found: ${method}`);
80
+ });
81
+
82
+ return lines;
83
+ }
package/src/tools.mjs ADDED
@@ -0,0 +1,147 @@
1
+ import {
2
+ allRows,
3
+ configuredKey,
4
+ createMyAiClient,
5
+ normalizeRange,
6
+ } from "./client.mjs";
7
+
8
+ const dateProperties = {
9
+ from: {
10
+ type: "string",
11
+ pattern: "^\\d{4}-\\d{2}-\\d{2}$",
12
+ description: "开始日期,YYYY-MM-DD。",
13
+ },
14
+ to: {
15
+ type: "string",
16
+ pattern: "^\\d{4}-\\d{2}-\\d{2}$",
17
+ description: "结束日期,YYYY-MM-DD。",
18
+ },
19
+ };
20
+
21
+ function inputSchema({ scope = false } = {}) {
22
+ return {
23
+ type: "object",
24
+ properties: {
25
+ ...dateProperties,
26
+ ...(scope
27
+ ? {
28
+ scope: {
29
+ type: "string",
30
+ enum: ["all", "platform", "self_reporting"],
31
+ default: "all",
32
+ },
33
+ }
34
+ : {}),
35
+ },
36
+ additionalProperties: false,
37
+ };
38
+ }
39
+
40
+ export const tools = [
41
+ {
42
+ name: "status",
43
+ description: "检查个人只读 Key 是否已配置,不返回 Key 内容。",
44
+ inputSchema: inputSchema(),
45
+ },
46
+ {
47
+ name: "get_my_ai_summary",
48
+ description: "查询本人 AI 使用汇总。",
49
+ inputSchema: inputSchema({ scope: true }),
50
+ },
51
+ {
52
+ name: "get_my_ai_details",
53
+ description: "查询本人完整 AI 使用明细。",
54
+ inputSchema: inputSchema({ scope: true }),
55
+ },
56
+ {
57
+ name: "get_my_ai_code_output",
58
+ description: "查询本人代码产出与代码活动量/元。",
59
+ inputSchema: inputSchema(),
60
+ },
61
+ {
62
+ name: "get_my_ai_autonomous_details",
63
+ description: "查询本人实时自主上报完整明细。",
64
+ inputSchema: inputSchema(),
65
+ },
66
+ {
67
+ name: "get_my_department_summary",
68
+ description: "查询当前授权部门范围的 AI 使用汇总。",
69
+ inputSchema: inputSchema({ scope: true }),
70
+ },
71
+ {
72
+ name: "get_my_department_details",
73
+ description: "查询当前授权部门范围的完整 AI 使用明细。",
74
+ inputSchema: inputSchema({ scope: true }),
75
+ },
76
+ {
77
+ name: "get_my_department_code_output",
78
+ description: "查询当前授权部门范围的代码产出与代码活动量/元。",
79
+ inputSchema: inputSchema(),
80
+ },
81
+ ];
82
+
83
+ const routes = {
84
+ get_my_ai_summary: {
85
+ path: "/api/my-ai/summary",
86
+ paged: false,
87
+ scope: true,
88
+ },
89
+ get_my_ai_details: {
90
+ path: "/api/my-ai/details",
91
+ paged: true,
92
+ scope: true,
93
+ },
94
+ get_my_ai_code_output: {
95
+ path: "/api/my-ai/code-output",
96
+ paged: false,
97
+ },
98
+ get_my_ai_autonomous_details: {
99
+ path: "/api/my-ai/autonomous/details",
100
+ paged: true,
101
+ },
102
+ get_my_department_summary: {
103
+ path: "/api/my-ai/department/summary",
104
+ paged: false,
105
+ scope: true,
106
+ },
107
+ get_my_department_details: {
108
+ path: "/api/my-ai/department/details",
109
+ paged: true,
110
+ scope: true,
111
+ },
112
+ get_my_department_code_output: {
113
+ path: "/api/my-ai/department/code-output",
114
+ paged: false,
115
+ },
116
+ };
117
+
118
+ export function createToolRuntime({ key = "", client, today } = {}) {
119
+ return {
120
+ async call(name, args = {}) {
121
+ if (name === "status") {
122
+ return {
123
+ configured: key.startsWith("aimq_"),
124
+ environment: "production",
125
+ };
126
+ }
127
+ const route = routes[name];
128
+ if (!route) throw new Error(`未知工具: ${name}`);
129
+ const resolvedKey = configuredKey({ AI_MANAGER_MY_AI_KEY: key });
130
+ const scope = args.scope || "all";
131
+ if (
132
+ route.scope &&
133
+ !["all", "platform", "self_reporting"].includes(scope)
134
+ ) {
135
+ throw new Error("scope 必须为 all、platform 或 self_reporting");
136
+ }
137
+ const params = {
138
+ ...normalizeRange(args, today),
139
+ ...(route.scope ? { scope } : {}),
140
+ };
141
+ const api = client || createMyAiClient({ key: resolvedKey });
142
+ return route.paged
143
+ ? { ...params, rows: await allRows(api.get, route.path, params) }
144
+ : api.get(route.path, params);
145
+ },
146
+ };
147
+ }