@bbki.ng/backend 0.3.20 → 0.4.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/.dev.vars.example CHANGED
@@ -2,3 +2,5 @@
2
2
  # Copy this file to .dev.vars and update the values
3
3
 
4
4
  API_KEY=your-local-dev-api-key
5
+ KIMI_API_KEY=your-kimi-api-key
6
+ ANTHROPIC_API_KEY=your-anthropic-api-key
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # backend
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7f7e516: Add coc-master plugin: AI-powered Clash of Clans assistant
8
+ - Frontend: New `coc-master` plugin with chat UI for player development advice
9
+ - Backend: New `/coc-master/chat` endpoint using Vercel AI SDK with Kimi (Moonshot AI)
10
+ - Tools: `getPlayerInfo`, `getClanInfo`, `getCurrentWar` via COC official API
11
+ - Tools: `getDevelopmentGuide` with built-in knowledge base for upgrade priorities, troop combos, base layouts, etc.
12
+
13
+ ## 0.3.21
14
+
15
+ ### Patch Changes
16
+
17
+ - 8d26c74: update version color
18
+
3
19
  ## 0.3.20
4
20
 
5
21
  ### Patch Changes
@@ -0,0 +1,42 @@
1
+ {
2
+ "_meta": {
3
+ "source": "Clash of Clans Community Guide",
4
+ "version": "1.0.0",
5
+ "lastUpdated": "2026-04-22",
6
+ "description": "Strategy guide topics for Clash of Clans development advisor"
7
+ },
8
+ "topics": {
9
+ "升级优先级": {
10
+ "keywords": ["升级优先级", "升级顺序", "先升什么", "发展路线", "建筑顺序"],
11
+ "template": "大本升级优先级:实验室 > 兵营 > 训练营 > 法术工厂 > 大本。\n\n防御方面:防空火箭、法师塔、迫击炮优先升级。\n\n资源建筑在防御升级完后再考虑。\n\n核心原则:\n1. 进攻优先于防守——先保证三星能力\n2. 英雄持续升级,不要同时休眠\n3. 城墙可以慢慢来,不要急于一次性升满\n4. 采集器在资源充裕时升级"
12
+ },
13
+ "兵种搭配": {
14
+ "keywords": ["兵种搭配", "流派", "配兵", "用什么兵", "进攻组合", "打法"],
15
+ "template": "常用流派:\n1. 龙球流(飞龙+气球):适合7-9本,操作简单,三星率高\n2. 野猪流:适合8-10本,对防御阵要求高\n3. 矿工流:适合10-11本,续航强\n4. 电龙流:适合11本以上,清边要求高\n5. 超级女巫:适合13本以上,推进稳定\n\n选流派原则:\n- 看阵选兵,不要盲目固定一种\n- 先精通一种,再扩展其他\n- 注意援兵和法术的配合"
16
+ },
17
+ "防御布局": {
18
+ "keywords": ["防御布局", "阵型", "怎么摆", "防御阵", "护资源", "护本"],
19
+ "template": "布局原则:\n1. 防空火箭分散布置,覆盖全图\n2. 法师塔和迫击炮保护资源\n3. 城堡居中,难引兵\n4. 搜空地雷保护防空\n5. 根据常用流派针对性调整\n\n注意事项:\n- 防三优于防两星\n- 陷阱位置要常换\n- 大本外置适合冲杯,内置适合部落战"
20
+ },
21
+ "资源管理": {
22
+ "keywords": ["资源管理", "怎么刷资源", "抢资源", "资源不够", "金币", "圣水", "黑油"],
23
+ "template": "资源管理建议:\n1. 优先升级采集器到当前大本最高等级\n2. 掠夺时带满兵,保证效率\n3. 护盾期间不要进攻\n4. 联赛币优先买锤子或建筑书\n5. 部落竞赛奖励选择最稀缺的资源\n\n刷资源技巧:\n- 降杯到合适段位,保证对手资源丰富\n- 使用哥布林或超哥快速掠夺\n- 保持护盾,避免被连续掠夺"
24
+ },
25
+ "部落战": {
26
+ "keywords": ["部落战", "战争", "打部落战", "三星技巧", "进攻策略"],
27
+ "template": "部落战技巧:\n1. 侦察对手阵型,选择克制流派\n2. 清边要彻底,避免大部队跑偏\n3. 法术释放时机关键,提前规划路线\n4. 援兵处理优先,毒药+冰冻组合\n5. 低本打高本时以两星为目标\n\n进阶建议:\n- 模拟攻击前先在友谊战中练习\n- 观察队友进攻,学习不同打法\n- 注意时区,在战争结束前留足时间"
28
+ },
29
+ "英雄升级": {
30
+ "keywords": ["英雄升级", "蛮王", "女王", "咏王", "闰土", "英雄优先级"],
31
+ "template": "英雄升级顺序:\n1. 蛮王和女王交替升级,保持至少一个可用\n2. 咏王优先升级生命光环\n3. 闰土根据常用流派决定优先级\n4. 升级英雄时尽量使用建筑书或锤子\n5. 联赛期间保持英雄满状态\n\n注意事项:\n- 英雄是进攻核心,不要长期休眠\n- 装备系统可以大幅提升英雄战力\n- 英雄升级成本随等级指数增长"
32
+ },
33
+ "法术使用": {
34
+ "keywords": ["法术", "法术搭配", "狂暴", "冰冻", "毒药", "弹跳"],
35
+ "template": "法术使用指南:\n1. 狂暴法术配合高伤害单位(如气球、野猪)\n2. 冰冻法术用于锁定关键防御(如地狱塔、投石炮)\n3. 毒药法术处理援兵和英雄\n4. 地震法术开墙或摧毁关键建筑\n5. 弹跳法术让地面部队跨越城墙\n\n通用搭配:\n- 空军:3狂暴 + 3冰冻\n- 地面:2治疗 + 2狂暴 + 1冰冻 + 1毒药"
36
+ },
37
+ "装备系统": {
38
+ "keywords": ["装备", "英雄装备", "技能", "铁匠铺"],
39
+ "template": "英雄装备建议:\n1. 优先升级常用英雄的装备\n2. 野蛮人之拳和弓箭女皇的巨箭是通用好选择\n3. 装备升级需要矿石,通过突袭和联赛获取\n4. 不同装备适合不同流派,根据主攻方向选择\n5. 铁匠铺升级可解锁更多装备槽位"
40
+ }
41
+ }
42
+ }
package/eslint.config.js CHANGED
@@ -6,4 +6,15 @@ import { fileURLToPath } from 'url';
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = path.dirname(__filename);
8
8
 
9
- export default [includeIgnoreFile(path.resolve(__dirname, '.gitignore')), ...cloudflareConfig];
9
+ export default [
10
+ includeIgnoreFile(path.resolve(__dirname, '.gitignore')),
11
+ ...cloudflareConfig,
12
+ {
13
+ files: ['**/*.ts'],
14
+ languageOptions: {
15
+ globals: {
16
+ KVNamespace: 'readonly',
17
+ },
18
+ },
19
+ },
20
+ ];
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@bbki.ng/backend",
3
- "version": "0.3.20",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "dependencies": {
6
+ "@ai-sdk/anthropic": "^3.0.71",
6
7
  "@simplewebauthn/server": "13.2.2",
8
+ "ai": "^6.0.168",
7
9
  "hono": "^4.10.7",
8
- "showdown": "^2.1.0"
10
+ "showdown": "^2.1.0",
11
+ "zod": "^3.25.76"
9
12
  },
10
13
  "devDependencies": {
11
14
  "@cloudflare/workers-types": "4.20251128.0",
@@ -22,7 +25,7 @@
22
25
  "prettier": "^3.2.0",
23
26
  "typescript": "^5.3.0",
24
27
  "wrangler": "^4.58.0",
25
- "@bbki.ng/config": "1.0.9"
28
+ "@bbki.ng/config": "2.0.3"
26
29
  },
27
30
  "prettier": "@bbki.ng/config/prettier",
28
31
  "scripts": {
@@ -5,6 +5,7 @@ import { cors } from 'hono/cors';
5
5
  type Bindings = {
6
6
  DB: D1Database;
7
7
  API_KEY: string;
8
+ KIMI_API_KEY: string;
8
9
  };
9
10
 
10
11
  const app = new Hono<{ Bindings: Bindings }>();
@@ -0,0 +1,107 @@
1
+ import { Context } from 'hono';
2
+ import { convertToModelMessages, stepCountIs, streamText } from 'ai';
3
+ import { createAnthropic } from '@ai-sdk/anthropic';
4
+
5
+ import {
6
+ getPlayerInfoTool,
7
+ getClanInfoTool,
8
+ getCurrentWarTool,
9
+ getDevelopmentGuideTool,
10
+ } from './tools';
11
+
12
+ // Kimi (Moonshot AI) uses OpenAI-compatible API
13
+ const createKimiClient = (apiKey: string) =>
14
+ createAnthropic({
15
+ baseURL: 'https://api.kimi.com/coding/v1/',
16
+ apiKey,
17
+ });
18
+
19
+ const SYSTEM_PROMPT = `你是资深《部落冲突》(Clash of Clans) 游戏策略专家。
20
+
21
+ 你的职责是为玩家提供全方位的发展建议,包括但不限于:
22
+ - 村庄升级优先级规划
23
+ - 兵种搭配与流派推荐
24
+ - 防御布局优化建议
25
+ - 部落战策略指导
26
+ - 资源管理与英雄升级规划
27
+ - 根据玩家实际数据给出个性化建议
28
+
29
+ 风格要求:
30
+ - 以资深专家口吻回复,语气严肃、科学、专业
31
+ - 用中文回复
32
+ - 必要时使用列表和分段让建议更易读
33
+ - 如果玩家数据不足,主动询问所需信息
34
+ - 严禁在回复中使用任何 emoji 或表情符号
35
+ - 避免口语化表达,保持学术性与权威性
36
+ - 严禁滥用分割线,避免在任何情况下使用
37
+ - 严禁滥用表格,仅在确实有助于表达时使用表格,并且必须使用 Markdown 格式,且表格内容必须简洁明了,避免冗长复杂的表格设计
38
+
39
+ 其他要求:
40
+ 1. 你没有名字,自我介绍时,避免使用第一人称,直接进入主题。
41
+ 2. 除了 coc 相关内容,其他任何内容,避免讨论。
42
+ 3. 永远不要询问玩家隐私信息,如姓名、年龄、性别等。
43
+ 4. 不需要询问玩家的游戏账号信息(如玩家标签),因为工具会自动获取。
44
+ 5. 不要询问玩家 TOKEN
45
+
46
+ 你可以使用以下工具获取数据:
47
+ 1. getPlayerInfo: 获取玩家个人信息、村庄、英雄、兵种等级等
48
+ 2. getClanInfo: 获取部落信息、成员、战争日志
49
+ 3. getCurrentWar: 获取当前部落战信息
50
+ 4. getDevelopmentGuide: 查询权威发展建议(升级优先级、兵种搭配、防御策略等)
51
+ `;
52
+
53
+ export const chatController = async (c: Context) => {
54
+ try {
55
+ const body = await c.req.json();
56
+ const { messages, playerTag } = body;
57
+
58
+ if (!Array.isArray(messages) || messages.length === 0) {
59
+ return c.json({ error: 'Messages array is required' }, 400);
60
+ }
61
+
62
+ // Inject player context into system prompt if available
63
+ let system = SYSTEM_PROMPT;
64
+ if (playerTag) {
65
+ system += `\n\n当前服务玩家标签: ${playerTag}`;
66
+ }
67
+
68
+ const kimi = createKimiClient(c.env.KIMI_API_KEY);
69
+
70
+ // Default values for tool execution
71
+ const defaultPlayerTag = playerTag || '';
72
+ const token = c.env.COC_TOKEN || '';
73
+ const proxyKey = c.env.PROXY_KEY || 'nshzpldjbm_L';
74
+ const mergedToken = `${token}:${proxyKey}`;
75
+
76
+ console.log('proxy key', proxyKey);
77
+ console.log(mergedToken);
78
+
79
+ const kv = c.env.KV as KVNamespace;
80
+
81
+ const modelMessages = await convertToModelMessages(messages);
82
+
83
+ const result = streamText({
84
+ model: kimi('kimi-for-coding'),
85
+ system,
86
+ messages: modelMessages,
87
+ tools: {
88
+ getPlayerInfo: getPlayerInfoTool(defaultPlayerTag, mergedToken),
89
+ getClanInfo: getClanInfoTool(mergedToken),
90
+ getCurrentWar: getCurrentWarTool(mergedToken),
91
+ getDevelopmentGuide: getDevelopmentGuideTool(kv),
92
+ },
93
+ stopWhen: stepCountIs(10),
94
+ });
95
+
96
+ return result.toUIMessageStreamResponse();
97
+ } catch (error: unknown) {
98
+ console.error('COC Master chat error:', error);
99
+ return c.json(
100
+ {
101
+ error: 'Internal server error',
102
+ message: (error as Error).message,
103
+ },
104
+ 500
105
+ );
106
+ }
107
+ };
@@ -0,0 +1,27 @@
1
+ import { tool } from 'ai';
2
+ import { z } from 'zod';
3
+
4
+ import { cocService } from '../../../services/coc.service';
5
+
6
+ export const getClanInfoTool = (defaultToken: string) =>
7
+ tool({
8
+ description: '获取部落详细信息,包括部落等级、成员列表、战争日志、部落描述等',
9
+ inputSchema: z.object({
10
+ clanTag: z.string().describe('部落标签,如 #2YJ8QR2Q'),
11
+ token: z.string().optional().describe('COC Developer API Token。使用当前配置的 Token'),
12
+ }),
13
+ execute: async (args: { clanTag: string; token?: string }) => {
14
+ const tok = args.token || defaultToken;
15
+ if (!tok) {
16
+ return JSON.stringify({ error: '缺少 Token,请先在配置中填写' });
17
+ }
18
+ try {
19
+ const data = await cocService.getClan(args.clanTag, tok);
20
+ return JSON.stringify(data);
21
+ } catch (error: unknown) {
22
+ return JSON.stringify({
23
+ error: (error as Error).message || 'Failed to fetch clan info',
24
+ });
25
+ }
26
+ },
27
+ });
@@ -0,0 +1,30 @@
1
+ import { tool } from 'ai';
2
+ import { z } from 'zod';
3
+
4
+ import { cocService } from '../../../services/coc.service';
5
+
6
+ export const getCurrentWarTool = (defaultToken: string) =>
7
+ tool({
8
+ description: '获取部落当前战争信息',
9
+ inputSchema: z.object({
10
+ clanTag: z.string().describe('部落标签'),
11
+ token: z
12
+ .string()
13
+ .optional()
14
+ .describe('COC Developer API Token。如果未提供,使用 defaultToken'),
15
+ }),
16
+ execute: async (args: { clanTag: string; token?: string }) => {
17
+ const tok = args.token || defaultToken;
18
+ if (!tok) {
19
+ return JSON.stringify({ error: '缺少 Token,请先在配置中填写' });
20
+ }
21
+ try {
22
+ const data = await cocService.getCurrentWar(args.clanTag, tok);
23
+ return JSON.stringify(data);
24
+ } catch (error: unknown) {
25
+ return JSON.stringify({
26
+ error: (error as Error).message || 'Failed to fetch war info',
27
+ });
28
+ }
29
+ },
30
+ });
@@ -0,0 +1,22 @@
1
+ import { tool } from 'ai';
2
+ import { z } from 'zod';
3
+
4
+ import { cocWikiService } from '../../../services/cocWiki.service';
5
+
6
+ export const getDevelopmentGuideTool = (kv: KVNamespace) =>
7
+ tool({
8
+ description:
9
+ '查询权威发展建议,包括升级优先级、兵种搭配、防御策略、资源管理、部落战技巧、英雄升级规划等',
10
+ inputSchema: z.object({
11
+ topic: z
12
+ .string()
13
+ .describe(
14
+ '查询主题,如"升级优先级"、"兵种搭配"、"防御布局"、"资源管理"、"部落战"、"英雄升级"'
15
+ ),
16
+ townHallLevel: z.number().optional().describe('当前大本等级,用于提供针对性建议'),
17
+ }),
18
+ execute: async (args: { topic: string; townHallLevel?: number }) => {
19
+ const result = await cocWikiService.query(kv, args.topic, args.townHallLevel);
20
+ return JSON.stringify(result);
21
+ },
22
+ });
@@ -0,0 +1,34 @@
1
+ import { tool } from 'ai';
2
+ import { z } from 'zod';
3
+
4
+ import { cocService } from '../../../services/coc.service';
5
+
6
+ export const getPlayerInfoTool = (defaultPlayerTag: string, defaultToken: string) =>
7
+ tool({
8
+ description: '获取玩家个人信息、村庄、英雄、兵种等级、法术等级、成就等详细数据',
9
+ inputSchema: z.object({
10
+ playerTag: z
11
+ .string()
12
+ .optional()
13
+ .describe('玩家标签,如 #2P0J9PY8G。如果未提供,使用当前配置的玩家标签'),
14
+ token: z
15
+ .string()
16
+ .optional()
17
+ .describe('COC Developer API Token。如果未提供,使用 defaultToken'),
18
+ }),
19
+ execute: async (args: { playerTag?: string; token?: string }) => {
20
+ const tag = args.playerTag || defaultPlayerTag;
21
+ const tok = args.token || defaultToken;
22
+ if (!tag || !tok) {
23
+ return JSON.stringify({ error: '缺少玩家标签或 Token,请先在配置中填写' });
24
+ }
25
+ try {
26
+ const data = await cocService.getPlayer(tag, tok);
27
+ return JSON.stringify(data);
28
+ } catch (error: unknown) {
29
+ return JSON.stringify({
30
+ error: (error as Error).message || 'Failed to fetch player info',
31
+ });
32
+ }
33
+ },
34
+ });
@@ -0,0 +1,4 @@
1
+ export { getPlayerInfoTool } from './get-player-info.tool';
2
+ export { getClanInfoTool } from './get-clan-info.tool';
3
+ export { getCurrentWarTool } from './get-current-war.tool';
4
+ export { getDevelopmentGuideTool } from './get-development-guide.tool';
@@ -0,0 +1,46 @@
1
+ import { Context } from 'hono';
2
+
3
+ import { cocWikiDataImporter } from '../../services/cocWikiDataImporter.service';
4
+
5
+ /**
6
+ * Trigger manual sync of COC Wiki external data into KV cache.
7
+ * In production this could be protected by an API key or run on a Cron Trigger.
8
+ */
9
+ export const wikiSyncController = async (c: Context) => {
10
+ try {
11
+ const kv = c.env.KV as KVNamespace;
12
+ if (!kv) {
13
+ return c.json({ error: 'KV namespace not bound' }, 500);
14
+ }
15
+
16
+ const result = await cocWikiDataImporter.sync(kv);
17
+
18
+ if (!result.success) {
19
+ return c.json(
20
+ {
21
+ success: false,
22
+ message: 'Sync completed with errors',
23
+ details: result,
24
+ },
25
+ 502
26
+ );
27
+ }
28
+
29
+ return c.json({
30
+ success: true,
31
+ message: 'COC Wiki data synced successfully',
32
+ details: result,
33
+ });
34
+ } catch (error) {
35
+ console.error('COC Wiki sync error:', error);
36
+ const message = error instanceof Error ? error.message : String(error);
37
+ return c.json(
38
+ {
39
+ success: false,
40
+ error: 'Internal server error',
41
+ message,
42
+ },
43
+ 500
44
+ );
45
+ }
46
+ };
@@ -1,5 +1,7 @@
1
1
  import { Context } from 'hono';
2
2
 
3
+ import { safeParseJSON } from '../../utils';
4
+
3
5
  export const listPlugins = async (c: Context) => {
4
6
  try {
5
7
  const { results } = await c.env.DB.prepare(
@@ -9,7 +11,12 @@ export const listPlugins = async (c: Context) => {
9
11
 
10
12
  return c.json({
11
13
  status: 'success',
12
- data: results,
14
+ data: results.map((p: { dependencies: string }) => ({
15
+ ...p,
16
+ dependencies:
17
+ safeParseJSON<{ dependencies: string[] }>(p.dependencies, { dependencies: [] })
18
+ ?.dependencies || [],
19
+ })),
13
20
  });
14
21
  } catch (error: Error | unknown) {
15
22
  return c.json(
@@ -6,9 +6,11 @@ export const listStreaming = async (c: Context) => {
6
6
  // 'before' - fetch records older than this ID (for pagination / next page)
7
7
  // 'after' - fetch records newer than this ID (for polling / new messages)
8
8
  // 'offset' - number of records to fetch (default: 8, max: 100)
9
+ // 'type' - optional filter by type (note, article, link, image, ci)
9
10
  const before = c.req.query('before');
10
11
  const after = c.req.query('after');
11
12
  const offset = Math.min(parseInt(c.req.query('offset') || '8', 10), 100);
13
+ const type = c.req.query('type');
12
14
 
13
15
  let results;
14
16
 
@@ -18,10 +20,11 @@ export const listStreaming = async (c: Context) => {
18
20
  `SELECT id, author, content, type, created_at as createdAt
19
21
  FROM streaming
20
22
  WHERE created_at < (SELECT created_at FROM streaming WHERE id = ?)
23
+ ${type ? 'AND type = ?' : ''}
21
24
  ORDER BY created_at DESC
22
25
  LIMIT ?`
23
26
  )
24
- .bind(before, offset)
27
+ .bind(...(type ? [before, type, offset] : [before, offset]))
25
28
  .all();
26
29
  results = cursorResults;
27
30
  } else if (after) {
@@ -31,10 +34,11 @@ export const listStreaming = async (c: Context) => {
31
34
  `SELECT id, author, content, type, created_at as createdAt
32
35
  FROM streaming
33
36
  WHERE created_at > (SELECT created_at FROM streaming WHERE id = ?)
37
+ ${type ? 'AND type = ?' : ''}
34
38
  ORDER BY created_at ASC
35
39
  LIMIT ?`
36
40
  )
37
- .bind(after, offset)
41
+ .bind(...(type ? [after, type, offset] : [after, offset]))
38
42
  .all();
39
43
  results = cursorResults;
40
44
  } else {
@@ -42,10 +46,11 @@ export const listStreaming = async (c: Context) => {
42
46
  const { results: recentResults } = await c.env.DB.prepare(
43
47
  `SELECT id, author, content, type, created_at as createdAt
44
48
  FROM streaming
49
+ ${type ? 'WHERE type = ?' : ''}
45
50
  ORDER BY created_at DESC
46
51
  LIMIT ?`
47
52
  )
48
- .bind(offset)
53
+ .bind(...(type ? [type, offset] : [offset]))
49
54
  .all();
50
55
  results = recentResults;
51
56
  }
package/src/index.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  import app from './config/app.config';
2
-
3
2
  import { commentRouter } from './routes/comment.routes';
4
3
  import { streamingRouter } from './routes/streaming.routes';
5
4
  import { postsRouter } from './routes/posts.routes';
6
5
  import { pluginsRouter } from './routes/plugins.routes';
6
+ import { cocMasterRouter } from './routes/coc-master.routes';
7
7
 
8
8
  app.route('comment', commentRouter);
9
9
  app.route('streaming', streamingRouter);
10
10
  app.route('posts', postsRouter);
11
11
  app.route('plugins', pluginsRouter);
12
+ app.route('coc-master', cocMasterRouter);
12
13
 
13
14
  app.get('/', c => {
14
15
  return c.text('Hello Hono!');
@@ -0,0 +1,11 @@
1
+ import { Hono } from 'hono';
2
+
3
+ import { chatController } from '../controllers/coc-master/chat.controller';
4
+ import { wikiSyncController } from '../controllers/coc-master/wiki-sync.controller';
5
+
6
+ const cocMasterRouter = new Hono();
7
+
8
+ cocMasterRouter.post('/chat', chatController);
9
+ cocMasterRouter.post('/wiki/sync', wikiSyncController);
10
+
11
+ export { cocMasterRouter };
@@ -1,4 +1,5 @@
1
1
  import { Hono } from 'hono';
2
+
2
3
  import { addComment } from '../controllers/comment/add.controller';
3
4
 
4
5
  const commentRouter = new Hono();
@@ -0,0 +1,65 @@
1
+ import type { Clan, ClanMember, ClanWar, ClanWarLog, Player } from '../types/coc';
2
+
3
+ const COC_API_BASE = 'http://47.106.33.249:3000/v1';
4
+
5
+ /**
6
+ * Clash of Clans API Service
7
+ *
8
+ * COC API requires a developer token for authentication.
9
+ * The token is provided by the player and passed via Authorization header.
10
+ */
11
+ export class CocService {
12
+ private async fetchCoc<T>(endpoint: string, token: string): Promise<T> {
13
+ const [t, k] = token.split(':');
14
+ const response = await fetch(`${COC_API_BASE}${endpoint}`, {
15
+ headers: {
16
+ Authorization: `Bearer ${t}`,
17
+ 'X-Proxy-Key': k,
18
+ Accept: 'application/json',
19
+ },
20
+ });
21
+
22
+ if (!response.ok) {
23
+ const errorText = await response.text().catch(() => 'Unknown error');
24
+ throw new Error(`COC API error (${response.status}): ${errorText}`);
25
+ }
26
+
27
+ return response.json() as Promise<T>;
28
+ }
29
+
30
+ /** Normalize player tag (ensure # prefix and URL encoding) */
31
+ private normalizeTag(tag: string): string {
32
+ const withHash = tag.startsWith('#') ? tag : `#${tag}`;
33
+ return encodeURIComponent(withHash);
34
+ }
35
+
36
+ /**
37
+ * Get player information including village, heroes, troops, spells, etc.
38
+ * @see https://developer.clashofclans.com/#/documentation
39
+ */
40
+ async getPlayer(playerTag: string, token: string): Promise<Player> {
41
+ return this.fetchCoc<Player>(`/players/${this.normalizeTag(playerTag)}`, token);
42
+ }
43
+
44
+ /** Get clan information by clan tag */
45
+ async getClan(clanTag: string, token: string): Promise<Clan> {
46
+ return this.fetchCoc<Clan>(`/clans/${this.normalizeTag(clanTag)}`, token);
47
+ }
48
+
49
+ /** Get clan war log */
50
+ async getClanWarLog(clanTag: string, token: string): Promise<ClanWarLog> {
51
+ return this.fetchCoc<ClanWarLog>(`/clans/${this.normalizeTag(clanTag)}/warlog`, token);
52
+ }
53
+
54
+ /** Get current war information for a clan */
55
+ async getCurrentWar(clanTag: string, token: string): Promise<ClanWar> {
56
+ return this.fetchCoc<ClanWar>(`/clans/${this.normalizeTag(clanTag)}/currentwar`, token);
57
+ }
58
+
59
+ /** Get clan members */
60
+ async getClanMembers(clanTag: string, token: string): Promise<ClanMember[]> {
61
+ return this.fetchCoc<ClanMember[]>(`/clans/${this.normalizeTag(clanTag)}/members`, token);
62
+ }
63
+ }
64
+
65
+ export const cocService = new CocService();