GitHub Actions commited on
Commit
ddf2ac8
·
1 Parent(s): 674adbd

Sync from GitHub (excluding README)

Browse files
Files changed (32) hide show
  1. .github/workflows/ai-review.yaml +116 -46
  2. hf_repo/.github/scripts/ai_agent.py +86 -79
  3. hf_repo/hf_repo/.github/workflows/ai-review.yaml +20 -116
  4. hf_repo/hf_repo/hf_repo/.github/scripts/ai_agent.py +140 -0
  5. hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +1 -1
  6. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +48 -42
  7. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +71 -60
  8. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +62 -24
  9. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +87 -0
  10. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/push_hf.yaml +47 -0
  11. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.gitignore +59 -0
  12. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/Dockerfile +16 -0
  13. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/LICENSE +21 -0
  14. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/cli/cli.js +389 -0
  15. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/app.js +444 -0
  16. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html +103 -0
  17. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/style.css +410 -0
  18. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/server.js +21 -0
  19. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docker-compose.yml +14 -0
  20. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.gitattributes +2 -0
  21. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/package-lock.json +675 -0
  22. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/package.json +31 -0
  23. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/auth.js +83 -0
  24. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/database.js +367 -0
  25. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/events.js +32 -0
  26. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/health.js +27 -0
  27. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/keys.js +195 -0
  28. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/server.js +70 -0
  29. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/server.js +134 -0
  30. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/websocket.js +63 -0
  31. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/start-server.bat +3 -0
  32. hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/start.sh +11 -0
.github/workflows/ai-review.yaml CHANGED
@@ -1,46 +1,116 @@
1
- name: AI Repository Agent (Python Tools)
2
-
3
- on:
4
- pull_request:
5
- types: [opened]
6
- issues:
7
- types: [opened]
8
- issue_comment:
9
- types: [created]
10
-
11
- jobs:
12
- ai-agent:
13
- if: |
14
- github.event_name == 'pull_request' ||
15
- github.event_name == 'issues' ||
16
- (github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '[use-ai]'))
17
- runs-on: ubuntu-latest
18
- permissions:
19
- contents: read
20
- pull-requests: write
21
- issues: write
22
-
23
- steps:
24
- - name: Checkout Code
25
- uses: actions/checkout@v4
26
- with:
27
- fetch-depth: 0
28
-
29
- - name: Set up Python
30
- uses: actions/setup-python@v4
31
- with:
32
- python-version: '3.10'
33
-
34
- - name: Install Dependencies
35
- run: |
36
- pip install openai PyGithub
37
-
38
- - name: Run AI Agent
39
- env:
40
- OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
41
- OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
42
- AI_MODEL: ${{ secrets.AI_MODEL }}
43
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44
- EVENT_CONTEXT: ${{ toJson(github.event) }}
45
- EVENT_NAME: ${{ github.event_name }}
46
- run: python .github/scripts/ai_agent.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import re
4
+ from openai import OpenAI
5
+ from github import Github, Auth
6
+
7
+ # 初始化客户端
8
+ auth = Auth.Token(os.getenv("GITHUB_TOKEN"))
9
+ gh = Github(auth=auth)
10
+ repo = gh.get_repo(os.getenv("GITHUB_REPOSITORY"))
11
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"))
12
+
13
+ event_data = json.loads(os.getenv("EVENT_CONTEXT"))
14
+ event_name = os.getenv("EVENT_NAME")
15
+
16
+ # --- 工具定义 (保持不变,增加 **kwargs 鲁棒性) ---
17
+ def list_directory(path=".", **kwargs):
18
+ try:
19
+ if ".." in path: return "Error: Access denied."
20
+ return "\n".join(os.listdir(path))
21
+ except Exception as e: return str(e)
22
+
23
+ def read_file(path, **kwargs):
24
+ try:
25
+ if ".." in path: return "Error: Access denied."
26
+ with open(path, 'r', encoding='utf-8') as f:
27
+ return f.read()[:5000]
28
+ except Exception as e: return str(e)
29
+
30
+ def search_keyword(keyword, path=".", **kwargs):
31
+ results = []
32
+ for root, _, files in os.walk(path):
33
+ if ".git" in root: continue
34
+ for file in files:
35
+ if file.endswith(('.py', '.js', '.ts', '.md', '.json', '.rs', '.yml')):
36
+ p = os.path.join(root, file)
37
+ try:
38
+ if keyword in open(p, 'r').read(): results.append(p)
39
+ except: continue
40
+ return "\n".join(results[:15]) if results else "No matches."
41
+
42
+ # --- 上下文准备 ---
43
+ def get_context():
44
+ if "pull_request" in event_data:
45
+ p = event_data["pull_request"]
46
+ return p["number"], f"PR @{p['user']['login']}\nTitle: {p['title']}\n{p['body']}"
47
+
48
+ i = event_data["issue"]
49
+ ctx = f"Issue @{i['user']['login']}\nTitle: {i['title']}\n{i['body']}"
50
+ if event_name == "issue_comment":
51
+ ctx += f"\n\nNew Comment by @{event_data['comment']['user']['login']}: {event_data['comment']['body']}"
52
+ return i["number"], ctx
53
+
54
+ issue_num, user_content = get_context()
55
+ issue_obj = repo.get_issue(number=issue_num)
56
+ repo_labels = [l.name for l in repo.get_labels()]
57
+
58
+ # --- AI 执行逻辑 ---
59
+ messages = [
60
+ {"role": "system", "content": f"你是一个高级仓库助手。可用标签: {repo_labels}。必须首行返回JSON: {{\"labels\":[], \"state\":\"open\"}},然后解释逻辑。"},
61
+ {"role": "user", "content": user_content}
62
+ ]
63
+
64
+ tools = [
65
+ {"type": "function", "function": {"name": "list_directory", "description": "List files", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
66
+ {"type": "function", "function": {"name": "read_file", "description": "Read content", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
67
+ {"type": "function", "function": {"name": "search_keyword", "description": "Search keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
68
+ ]
69
+
70
+ # 允许最多 5 轮工具交互
71
+ for i in range(5):
72
+ response = client.chat.completions.create(
73
+ model=os.getenv("AI_MODEL"),
74
+ messages=messages,
75
+ tools=tools,
76
+ temperature=0
77
+ )
78
+
79
+ # 核心修复:统一将模型返回的消息转为可序列化的字典格式
80
+ msg = response.choices[0].message
81
+ msg_dict = msg.model_dump()
82
+ messages.append(msg_dict)
83
+
84
+ if not msg.tool_calls:
85
+ break
86
+
87
+ for tool_call in msg.tool_calls:
88
+ args = json.loads(tool_call.function.arguments)
89
+ func = {"list_directory": list_directory, "read_file": read_file, "search_keyword": search_keyword}.get(tool_call.function.name)
90
+ result = func(**args) if func else "Unknown function"
91
+ messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": str(result)})
92
+
93
+ # 确保最后一条消息是文本回复
94
+ if messages[-1].get("role") == "tool" or (messages[-1].get("tool_calls") and not messages[-1].get("content")):
95
+ final_check = client.chat.completions.create(model=os.getenv("AI_MODEL"), messages=messages)
96
+ messages.append(final_check.choices[0].message.model_dump())
97
+
98
+ # 提取最终文本内容
99
+ final_msg = messages[-1]
100
+ final_res = final_msg.get("content") or ""
101
+
102
+ # --- 结果解析与执行 ---
103
+ json_data = {"labels": [], "state": "open"}
104
+ match = re.search(r'(\{.*?\})', final_res, re.DOTALL)
105
+ if match:
106
+ try:
107
+ json_data = json.loads(match.group(1))
108
+ final_res = final_res.replace(match.group(1), "").strip()
109
+ except: pass
110
+
111
+ if json_data.get("labels"):
112
+ issue_obj.add_to_labels(*json_data["labels"])
113
+ if json_data.get("state") in ["open", "closed"]:
114
+ issue_obj.edit(state=json_data["state"])
115
+
116
+ issue_obj.create_comment(f"### 🤖 AI Agent Execution\n\n{final_res}")
hf_repo/.github/scripts/ai_agent.py CHANGED
@@ -1,106 +1,110 @@
1
  import os
2
  import json
3
- import base64
4
  from openai import OpenAI
5
- from github import Github
6
 
7
- # 初始化客户端
8
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"))
9
- gh = Github(os.getenv("GITHUB_TOKEN"))
10
  repo = gh.get_repo(os.getenv("GITHUB_REPOSITORY"))
 
 
11
  event_data = json.loads(os.getenv("EVENT_CONTEXT"))
12
  event_name = os.getenv("EVENT_NAME")
13
 
14
- # --- 定义 AI 可调用的工具 ---
15
 
16
- def list_directory(path="."):
17
  """列出指定目录下的文件和文件夹"""
18
  try:
 
 
19
  items = os.listdir(path)
20
  return "\n".join(items)
21
  except Exception as e:
22
- return str(e)
23
 
24
- def read_file(path):
25
  """读取特定文件的完整内容"""
26
  try:
 
27
  with open(path, 'r', encoding='utf-8') as f:
28
- return f.read()[:5000] # 限制长度防止 Over-token
29
  except Exception as e:
30
- return str(e)
31
 
32
- def search_keyword(keyword, path="."):
33
  """在当前目录及其子目录中搜索关键词"""
34
  results = []
35
- for root, dirs, files in os.walk(path):
36
- for file in files:
37
- if file.endswith(('.py', '.js', '.ts', '.md', '.json', '.rs')):
38
- full_path = os.path.join(root, file)
39
- try:
40
- with open(full_path, 'r', encoding='utf-8') as f:
41
- if keyword in f.read():
42
- results.append(full_path)
43
- except:
44
- continue
45
- return "\n".join(results[:20])
46
-
47
- # --- 准备上下文与角色 ---
 
 
 
48
 
49
  def get_context():
50
- if event_name == "pull_request":
51
- number = event_data["pull_request"]["number"]
52
- author = event_data["pull_request"]["user"]["login"]
53
- title = event_data["pull_request"]["title"]
54
- body = event_data["pull_request"]["body"]
55
- return number, f"PR Author: @{author}\nTitle: {title}\nBody: {body}\n(This is a Pull Request)"
56
 
57
- number = event_data["issue"]["number"]
58
- author = event_data["issue"]["user"]["login"]
59
- title = event_data["issue"]["title"]
60
- body = event_data["issue"]["body"]
61
 
62
  if event_name == "issue_comment":
63
  actor = event_data["comment"]["user"]["login"]
64
  cmd = event_data["comment"]["body"]
65
- return number, f"Issue Author: @{author}\nTriggered by: @{actor}\nCommand: {cmd}\nContext: {title}\n{body}"
66
 
67
- return number, f"Issue Author: @{author}\nTitle: {title}\nBody: {body}"
68
 
69
  issue_num, user_content = get_context()
70
  issue_obj = repo.get_issue(number=issue_num)
71
  repo_labels = [l.name for l in repo.get_labels()]
72
 
73
- # --- 主逻辑 ---
74
 
75
  messages = [
76
  {"role": "system", "content": f"""你是一个高级仓库助手 (@github-actions[bot])。
77
- 你可以通过工具阅读代码、搜索文件并管理 Issue/PR 状态。
78
 
79
  可用标签: {repo_labels}
80
- 你的目标:
81
- 1. 理解用户意图。
82
- 2. 如果需要,使用工具查看项目结构或具体文件。
83
- 3. 给出处理方案,并直接执行(打标签、关闭等)。
84
 
85
- 输出规范:
86
- 回复开头必须是 JSON 指令: {{"labels": [], "state": "open"|"closed"}}
87
- 然后是你的执行报告。"""},
 
88
  {"role": "user", "content": user_content}
89
  ]
90
 
91
- # 工具定义
92
  tools = [
93
- {"type": "function", "function": {"name": "list_directory", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
94
- {"type": "function", "function": {"name": "read_file", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
95
- {"type": "function", "function": {"name": "search_keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
96
  ]
97
 
98
- # AI 决策循环 (允许最多 3 次工具调用)
99
  for _ in range(3):
100
  response = client.chat.completions.create(
101
  model=os.getenv("AI_MODEL"),
102
  messages=messages,
103
- tools=tools
 
104
  )
105
  msg = response.choices[0].message
106
  messages.append(msg)
@@ -109,32 +113,35 @@ for _ in range(3):
109
  break
110
 
111
  for tool_call in msg.tool_calls:
112
- func_name = tool_call.function.name
113
- args = json.loads(tool_call.function.arguments)
114
 
115
- if func_name == "list_directory": result = list_directory(**args)
116
- elif func_name == "read_file": result = read_file(**args)
117
- elif func_name == "search_keyword": result = search_keyword(**args)
 
 
 
118
 
119
- messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
120
-
121
- # 解析结果并操作 GitHub
122
- final_text = messages[-1].content
123
- json_part = {}
124
- try:
125
- if final_text.startswith("{"):
126
- import re
127
- match = re.search(r'(\{.*?\})', final_text, re.DOTALL)
128
- if match:
129
- json_part = json.loads(match.group(1))
130
- final_text = final_text.replace(match.group(1), "").strip()
131
- except:
132
- pass
133
-
134
- # 执行 GitHub 动作
135
- if json_part.get("labels"):
136
- issue_obj.add_to_labels(*json_part["labels"])
137
- if json_part.get("state"):
138
- issue_obj.edit(state=json_part["state"])
139
-
140
- issue_obj.create_comment(f"### 🤖 AI Agent Action\n\n{final_text}")
 
1
  import os
2
  import json
3
+ import re
4
  from openai import OpenAI
5
+ from github import Github, Auth # 导入 Auth 以修复弃用警告
6
 
7
+ # --- 1. 初始化客户端 (修复 DeprecationWarning) ---
8
+ auth = Auth.Token(os.getenv("GITHUB_TOKEN"))
9
+ gh = Github(auth=auth)
10
  repo = gh.get_repo(os.getenv("GITHUB_REPOSITORY"))
11
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"))
12
+
13
  event_data = json.loads(os.getenv("EVENT_CONTEXT"))
14
  event_name = os.getenv("EVENT_NAME")
15
 
16
+ # --- 2. 定义工具 (增加 **kwargs 以忽略多余参数) ---
17
 
18
+ def list_directory(path=".", **kwargs):
19
  """列出指定目录下的文件和文件夹"""
20
  try:
21
+ # 基础路径安全检查
22
+ if ".." in path: return "Error: Cannot access parent directory."
23
  items = os.listdir(path)
24
  return "\n".join(items)
25
  except Exception as e:
26
+ return f"Error listing directory: {str(e)}"
27
 
28
+ def read_file(path, **kwargs):
29
  """读取特定文件的完整内容"""
30
  try:
31
+ if ".." in path: return "Error: Access denied."
32
  with open(path, 'r', encoding='utf-8') as f:
33
+ return f.read()[:5000]
34
  except Exception as e:
35
+ return f"Error reading file: {str(e)}"
36
 
37
+ def search_keyword(keyword, path=".", **kwargs):
38
  """在当前目录及其子目录中搜索关键词"""
39
  results = []
40
+ try:
41
+ for root, dirs, files in os.walk(path):
42
+ if ".git" in root: continue # 过滤 Git 目录
43
+ for file in files:
44
+ if file.endswith(('.py', '.js', '.ts', '.md', '.json', '.rs', '.toml', '.yml')):
45
+ full_path = os.path.join(root, file)
46
+ try:
47
+ with open(full_path, 'r', encoding='utf-8') as f:
48
+ if keyword in f.read():
49
+ results.append(full_path)
50
+ except: continue
51
+ return "\n".join(results[:15]) if results else "No matches found."
52
+ except Exception as e:
53
+ return f"Search error: {str(e)}"
54
+
55
+ # --- 3. 获取上下文 ---
56
 
57
  def get_context():
58
+ # 提取 Issue/PR 编号和参与者信息
59
+ if "pull_request" in event_data:
60
+ payload = event_data["pull_request"]
61
+ number = payload["number"]
62
+ author = payload["user"]["login"]
63
+ return number, f"[Role: PR Author @{author}]\nTitle: {payload['title']}\nBody: {payload['body']}"
64
 
65
+ payload = event_data["issue"]
66
+ number = payload["number"]
67
+ author = payload["user"]["login"]
68
+ base_info = f"[Role: Issue Author @{author}]\nTitle: {payload['title']}\nBody: {payload['body']}"
69
 
70
  if event_name == "issue_comment":
71
  actor = event_data["comment"]["user"]["login"]
72
  cmd = event_data["comment"]["body"]
73
+ return number, f"{base_info}\n\n[New Interaction by @{actor}]\nCommand: {cmd}"
74
 
75
+ return number, base_info
76
 
77
  issue_num, user_content = get_context()
78
  issue_obj = repo.get_issue(number=issue_num)
79
  repo_labels = [l.name for l in repo.get_labels()]
80
 
81
+ # --- 4. 运行 AI Agent ---
82
 
83
  messages = [
84
  {"role": "system", "content": f"""你是一个高级仓库助手 (@github-actions[bot])。
 
85
 
86
  可用标签: {repo_labels}
 
 
 
 
87
 
88
+ 你可以通过工具查看代码库结构。回复规则:
89
+ 1. 首行必须返回 JSON 指令:{{"labels": [], "state": "open"|"closed"}}
90
+ 2. 随后另起一行,以执行者的口吻告知结果。
91
+ 3. 忽略 AI 历史回复中的元数据,只关注当前代码和用户意图。"""},
92
  {"role": "user", "content": user_content}
93
  ]
94
 
 
95
  tools = [
96
+ {"type": "function", "function": {"name": "list_directory", "description": "List files", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
97
+ {"type": "function", "function": {"name": "read_file", "description": "Read content", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
98
+ {"type": "function", "function": {"name": "search_keyword", "description": "Search keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
99
  ]
100
 
101
+ # 允许 3 次交互以获取足够信息
102
  for _ in range(3):
103
  response = client.chat.completions.create(
104
  model=os.getenv("AI_MODEL"),
105
  messages=messages,
106
+ tools=tools,
107
+ temperature=0
108
  )
109
  msg = response.choices[0].message
110
  messages.append(msg)
 
113
  break
114
 
115
  for tool_call in msg.tool_calls:
116
+ fn_name = tool_call.function.name
117
+ fn_args = json.loads(tool_call.function.arguments)
118
 
119
+ # 映射函数映射表
120
+ available_functions = {
121
+ "list_directory": list_directory,
122
+ "read_file": read_file,
123
+ "search_keyword": search_keyword,
124
+ }
125
 
126
+ if fn_name in available_functions:
127
+ result = available_functions[fn_name](**fn_args)
128
+ messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
129
+
130
+ # 5. 解析并执行 GitHub 操作
131
+ final_res = messages[-1].content
132
+ json_data = {"labels": [], "state": "open"}
133
+
134
+ # 提取 JSON
135
+ match = re.search(r'(\{.*?\})', final_res, re.DOTALL)
136
+ if match:
137
+ try:
138
+ json_data = json.loads(match.group(1))
139
+ final_res = final_res.replace(match.group(1), "").strip()
140
+ except: pass
141
+
142
+ if json_data.get("labels"):
143
+ issue_obj.add_to_labels(*json_data["labels"])
144
+ if json_data.get("state") and json_data["state"] in ["open", "closed"]:
145
+ issue_obj.edit(state=json_data["state"])
146
+
147
+ issue_obj.create_comment(f"### 🤖 AI Agent Execution\n\n{final_res}")
hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,4 +1,4 @@
1
- name: AI Repository Assistant
2
 
3
  on:
4
  pull_request:
@@ -9,11 +9,11 @@ on:
9
  types: [created]
10
 
11
  jobs:
12
- ai-assistant:
13
  if: |
14
  github.event_name == 'pull_request' ||
15
  github.event_name == 'issues' ||
16
- startsWith(github.event.comment.body, '[use-ai]')
17
  runs-on: ubuntu-latest
18
  permissions:
19
  contents: read
@@ -23,120 +23,24 @@ jobs:
23
  steps:
24
  - name: Checkout Code
25
  uses: actions/checkout@v4
26
-
27
- - name: Prepare Context
28
- id: prep
29
- run: |
30
- # 提取基本信息
31
- ISSUE_AUTHOR="${{ github.event.issue.user.login || github.event.pull_request.user.login }}"
32
- CURRENT_ACTOR="${{ github.actor }}"
33
-
34
- echo "### METADATA ###" > content.txt
35
- echo "Issue/PR Author: @$ISSUE_AUTHOR" >> content.txt
36
- echo "Instruction triggered by: @$CURRENT_ACTOR" >> content.txt
37
- echo "Assistant Identity: @github-actions[bot]" >> content.txt
38
- echo -e "------------------\n" >> content.txt
39
-
40
- if [ "${{ github.event_name }}" == "issue_comment" ]; then
41
- echo "### NEW COMMAND FROM @$CURRENT_ACTOR ###" >> content.txt
42
- echo "Comment: ${{ github.event.comment.body }}" >> content.txt
43
- echo -e "\n### ORIGINAL CONTEXT ###" >> content.txt
44
- echo "Title: ${{ github.event.issue.title }}" >> content.txt
45
- echo "Description: ${{ github.event.issue.body }}" >> content.txt
46
- elif [ "${{ github.event_name }}" == "pull_request" ]; then
47
- echo "### NEW PULL REQUEST FROM @$ISSUE_AUTHOR ###" >> content.txt
48
- git fetch origin ${{ github.event.pull_request.base.ref }}
49
- git diff origin/${{ github.event.pull_request.base.ref }}...HEAD > pr_diff.txt
50
- cat pr_diff.txt >> content.txt
51
- else
52
- echo "### NEW ISSUE FROM @$ISSUE_AUTHOR ###" >> content.txt
53
- echo "Title: ${{ github.event.issue.title }}" >> content.txt
54
- echo "Body: ${{ github.event.issue.body }}" >> content.txt
55
- fi
56
-
57
- echo "number=${{ github.event.issue.number || github.event.pull_request.number }}" >> $GITHUB_OUTPUT
58
- echo "type=${{ github.event_name }}" >> $GITHUB_OUTPUT
59
-
60
- - name: AI Action Execution
61
- uses: actions/github-script@v7
62
- env:
63
- API_KEY: ${{ secrets.OPENAI_API_KEY }}
64
- BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
65
- MODEL: ${{ secrets.AI_MODEL }}
66
- ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
67
  with:
68
- script: |
69
- const fs = require('fs');
70
- const content = fs.readFileSync('content.txt', 'utf8');
71
-
72
- const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
73
- owner: context.repo.owner,
74
- repo: context.repo.repo,
75
- });
76
- const labelNames = repoLabels.map(l => l.name);
77
-
78
- const prompt = `你是一个拥有自主权的仓库助手 (@github-actions[bot])。
79
- 你需要根据提供的【METADATA】区分对话中的不同角色。
80
-
81
- 角色指南:
82
- 1. Issue/PR Author: 任务的发起者。
83
- 2. Instruction triggered by: 当前向你下达指令的人。如果是 Author 且内容包含 "[use-ai]",说明他在请求你介入。
84
- 3. Assistant Identity: 这是你自己。请不要审视或评价你自己的历史回复。
85
-
86
- 你的权力:
87
- - 根据 Issue 内容的质量自动打标签(可选:${labelNames.join(", ")})。
88
- - 如果内容是无意义的测试、违反规范或已解决,请直接关闭它。
89
- - 你的语气应该是果断的执行者,而不是卑微的助理。
90
-
91
- 输出要求(严格 JSON 第一行):
92
- {"labels": ["label_name"], "state": "closed" | "open"}
93
- 然后另起一行说明你的处理逻辑。`;
94
 
95
- try {
96
- const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
97
- method: 'POST',
98
- headers: { 'Authorization': `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
99
- body: JSON.stringify({
100
- model: process.env.MODEL,
101
- messages: [{ role: "user", content: prompt + "\n\n内容如下:\n" + content }],
102
- temperature: 0.1 // 降低随机性,增强逻辑判断
103
- })
104
- });
105
-
106
- const data = await response.json();
107
- const fullText = data.choices[0].message.content;
108
-
109
- const jsonMatch = fullText.match(/^\{.*?\}/);
110
- let config = { labels: [], state: "open" };
111
- let commentBody = fullText;
112
-
113
- if (jsonMatch) {
114
- config = JSON.parse(jsonMatch[0]);
115
- commentBody = fullText.replace(jsonMatch[0], "").trim();
116
- }
117
-
118
- const issueParams = {
119
- owner: context.repo.owner,
120
- repo: context.repo.repo,
121
- issue_number: parseInt(process.env.ISSUE_NUMBER)
122
- };
123
-
124
- // 执行打标签
125
- if (config.labels && config.labels.length > 0) {
126
- await github.rest.issues.addLabels({ ...issueParams, labels: config.labels });
127
- }
128
-
129
- // 执行状态变更
130
- if (config.state) {
131
- await github.rest.issues.update({ ...issueParams, state: config.state });
132
- }
133
 
134
- // 发表回复
135
- await github.rest.issues.createComment({
136
- ...issueParams,
137
- body: `### 🤖 AI Assistant Action\n\n${commentBody}`
138
- });
139
 
140
- } catch (err) {
141
- core.setFailed(err.message);
142
- }
 
 
 
 
 
 
 
1
+ name: AI Repository Agent (Python Tools)
2
 
3
  on:
4
  pull_request:
 
9
  types: [created]
10
 
11
  jobs:
12
+ ai-agent:
13
  if: |
14
  github.event_name == 'pull_request' ||
15
  github.event_name == 'issues' ||
16
+ (github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '[use-ai]'))
17
  runs-on: ubuntu-latest
18
  permissions:
19
  contents: read
 
23
  steps:
24
  - name: Checkout Code
25
  uses: actions/checkout@v4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  with:
27
+ fetch-depth: 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
+ - name: Set up Python
30
+ uses: actions/setup-python@v4
31
+ with:
32
+ python-version: '3.10'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
+ - name: Install Dependencies
35
+ run: |
36
+ pip install openai PyGithub
 
 
37
 
38
+ - name: Run AI Agent
39
+ env:
40
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
41
+ OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
42
+ AI_MODEL: ${{ secrets.AI_MODEL }}
43
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44
+ EVENT_CONTEXT: ${{ toJson(github.event) }}
45
+ EVENT_NAME: ${{ github.event_name }}
46
+ run: python .github/scripts/ai_agent.py
hf_repo/hf_repo/hf_repo/.github/scripts/ai_agent.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import base64
4
+ from openai import OpenAI
5
+ from github import Github
6
+
7
+ # 初始化客户端
8
+ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"))
9
+ gh = Github(os.getenv("GITHUB_TOKEN"))
10
+ repo = gh.get_repo(os.getenv("GITHUB_REPOSITORY"))
11
+ event_data = json.loads(os.getenv("EVENT_CONTEXT"))
12
+ event_name = os.getenv("EVENT_NAME")
13
+
14
+ # --- 定义 AI 可调用的工具 ---
15
+
16
+ def list_directory(path="."):
17
+ """列出指定目录下的文件和文件夹"""
18
+ try:
19
+ items = os.listdir(path)
20
+ return "\n".join(items)
21
+ except Exception as e:
22
+ return str(e)
23
+
24
+ def read_file(path):
25
+ """读取特定文件的完整内容"""
26
+ try:
27
+ with open(path, 'r', encoding='utf-8') as f:
28
+ return f.read()[:5000] # 限制长度防止 Over-token
29
+ except Exception as e:
30
+ return str(e)
31
+
32
+ def search_keyword(keyword, path="."):
33
+ """在当前目录及其子目录中搜索关键词"""
34
+ results = []
35
+ for root, dirs, files in os.walk(path):
36
+ for file in files:
37
+ if file.endswith(('.py', '.js', '.ts', '.md', '.json', '.rs')):
38
+ full_path = os.path.join(root, file)
39
+ try:
40
+ with open(full_path, 'r', encoding='utf-8') as f:
41
+ if keyword in f.read():
42
+ results.append(full_path)
43
+ except:
44
+ continue
45
+ return "\n".join(results[:20])
46
+
47
+ # --- 准备上下文与角色 ---
48
+
49
+ def get_context():
50
+ if event_name == "pull_request":
51
+ number = event_data["pull_request"]["number"]
52
+ author = event_data["pull_request"]["user"]["login"]
53
+ title = event_data["pull_request"]["title"]
54
+ body = event_data["pull_request"]["body"]
55
+ return number, f"PR Author: @{author}\nTitle: {title}\nBody: {body}\n(This is a Pull Request)"
56
+
57
+ number = event_data["issue"]["number"]
58
+ author = event_data["issue"]["user"]["login"]
59
+ title = event_data["issue"]["title"]
60
+ body = event_data["issue"]["body"]
61
+
62
+ if event_name == "issue_comment":
63
+ actor = event_data["comment"]["user"]["login"]
64
+ cmd = event_data["comment"]["body"]
65
+ return number, f"Issue Author: @{author}\nTriggered by: @{actor}\nCommand: {cmd}\nContext: {title}\n{body}"
66
+
67
+ return number, f"Issue Author: @{author}\nTitle: {title}\nBody: {body}"
68
+
69
+ issue_num, user_content = get_context()
70
+ issue_obj = repo.get_issue(number=issue_num)
71
+ repo_labels = [l.name for l in repo.get_labels()]
72
+
73
+ # --- 主逻辑 ---
74
+
75
+ messages = [
76
+ {"role": "system", "content": f"""你是一个高级仓库助手 (@github-actions[bot])。
77
+ 你可以通过工具阅读代码、搜索文件并管理 Issue/PR 状态。
78
+
79
+ 可用标签: {repo_labels}
80
+ 你的目标:
81
+ 1. 理解用户意图。
82
+ 2. 如果需要,使用工具查看项目结构或具体文件。
83
+ 3. 给出处理方案,并直接执行(打标签、关闭等)。
84
+
85
+ 输出规范:
86
+ 回复开头必须是 JSON 指令: {{"labels": [], "state": "open"|"closed"}}
87
+ 然后是你的执行报告。"""},
88
+ {"role": "user", "content": user_content}
89
+ ]
90
+
91
+ # 工具定义
92
+ tools = [
93
+ {"type": "function", "function": {"name": "list_directory", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
94
+ {"type": "function", "function": {"name": "read_file", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}}}},
95
+ {"type": "function", "function": {"name": "search_keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
96
+ ]
97
+
98
+ # AI 决策循环 (允许最多 3 次工具调用)
99
+ for _ in range(3):
100
+ response = client.chat.completions.create(
101
+ model=os.getenv("AI_MODEL"),
102
+ messages=messages,
103
+ tools=tools
104
+ )
105
+ msg = response.choices[0].message
106
+ messages.append(msg)
107
+
108
+ if not msg.tool_calls:
109
+ break
110
+
111
+ for tool_call in msg.tool_calls:
112
+ func_name = tool_call.function.name
113
+ args = json.loads(tool_call.function.arguments)
114
+
115
+ if func_name == "list_directory": result = list_directory(**args)
116
+ elif func_name == "read_file": result = read_file(**args)
117
+ elif func_name == "search_keyword": result = search_keyword(**args)
118
+
119
+ messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": result})
120
+
121
+ # 解析结果并操作 GitHub
122
+ final_text = messages[-1].content
123
+ json_part = {}
124
+ try:
125
+ if final_text.startswith("{"):
126
+ import re
127
+ match = re.search(r'(\{.*?\})', final_text, re.DOTALL)
128
+ if match:
129
+ json_part = json.loads(match.group(1))
130
+ final_text = final_text.replace(match.group(1), "").strip()
131
+ except:
132
+ pass
133
+
134
+ # 执行 GitHub 动作
135
+ if json_part.get("labels"):
136
+ issue_obj.add_to_labels(*json_part["labels"])
137
+ if json_part.get("state"):
138
+ issue_obj.edit(state=json_part["state"])
139
+
140
+ issue_obj.create_comment(f"### 🤖 AI Agent Action\n\n{final_text}")
hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,4 +1,4 @@
1
- name: AI Repository Assistant (Role-Aware)
2
 
3
  on:
4
  pull_request:
 
1
+ name: AI Repository Assistant
2
 
3
  on:
4
  pull_request:
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,4 +1,4 @@
1
- name: AI Repository Assistant
2
 
3
  on:
4
  pull_request:
@@ -6,11 +6,10 @@ on:
6
  issues:
7
  types: [opened]
8
  issue_comment:
9
- types: [created] # 支持通过评论触发
10
 
11
  jobs:
12
  ai-assistant:
13
- # 仅在:1. 新开 PR/Issue 2. 评论以 [use-ai] 开头时运行
14
  if: |
15
  github.event_name == 'pull_request' ||
16
  github.event_name == 'issues' ||
@@ -28,25 +27,35 @@ jobs:
28
  - name: Prepare Context
29
  id: prep
30
  run: |
31
- # 判断是 PR 还是 Issue
32
- if [ "${{ github.event.pull_request }}" != "" ]; then
33
- EVENT_TYPE="PR"
34
- ISSUE_NUMBER="${{ github.event.pull_request.number }}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  git fetch origin ${{ github.event.pull_request.base.ref }}
36
- git diff origin/${{ github.event.pull_request.base.ref }}...HEAD > content.txt
 
37
  else
38
- EVENT_TYPE="Issue"
39
- ISSUE_NUMBER="${{ github.event.issue.number }}"
40
- # 如果是评论触发,获取评论内容作为补充指令
41
- if [ "${{ github.event_name }}" == "issue_comment" ]; then
42
- echo "Command: ${{ github.event.comment.body }}" > content.txt
43
- echo -e "\nOriginal Content:\n${{ github.event.issue.title }}\n${{ github.event.issue.body }}" >> content.txt
44
- else
45
- echo -e "Title: ${{ github.event.issue.title }}\n\n${{ github.event.issue.body }}" > content.txt
46
- fi
47
  fi
48
- echo "type=$EVENT_TYPE" >> $GITHUB_OUTPUT
49
- echo "number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
 
50
 
51
  - name: AI Action Execution
52
  uses: actions/github-script@v7
@@ -54,35 +63,34 @@ jobs:
54
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
55
  BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
56
  MODEL: ${{ secrets.AI_MODEL }}
57
- EVENT_TYPE: ${{ steps.prep.outputs.type }}
58
  ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
59
  with:
60
  script: |
61
  const fs = require('fs');
62
  const content = fs.readFileSync('content.txt', 'utf8');
63
 
64
- // 1. 获取现有 Labels
65
  const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
66
  owner: context.repo.owner,
67
  repo: context.repo.repo,
68
  });
69
  const labelNames = repoLabels.map(l => l.name);
70
 
71
- // 2. 角色设定:从“建议者”转变为“执行者”
72
- const prompt = `你是一个自动化的仓库助手。你被授权管理该仓库的 Issues 和 PR。
73
- 你的任务:分析内容,打上标签,并决定是否关闭或保持开启。
74
-
75
- 仓库现有标签:[${labelNames.join(", ")}]。
76
-
77
- 输出要求:
78
- 必须在回复的第一行输出 JSON 格式的操作指令:
79
- {"labels": ["label1"], "state": "closed" | "open"}
80
 
81
- 随后另起一行,以助手的身份简洁地说明你执行的操作和理由。不要说“我建议”,要说“我已执行”。
82
-
83
- 当前环境:${process.env.EVENT_TYPE}
84
- 内容详情:
85
- ${content}`;
 
 
 
 
 
 
 
 
86
 
87
  try {
88
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
@@ -90,15 +98,14 @@ jobs:
90
  headers: { 'Authorization': `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
91
  body: JSON.stringify({
92
  model: process.env.MODEL,
93
- messages: [{ role: "user", content: prompt }],
94
- temperature: 0.2
95
  })
96
  });
97
 
98
  const data = await response.json();
99
  const fullText = data.choices[0].message.content;
100
 
101
- // 3. 解析指令
102
  const jsonMatch = fullText.match(/^\{.*?\}/);
103
  let config = { labels: [], state: "open" };
104
  let commentBody = fullText;
@@ -114,23 +121,22 @@ jobs:
114
  issue_number: parseInt(process.env.ISSUE_NUMBER)
115
  };
116
 
117
- // 4. 执行操作:打标签
118
  if (config.labels && config.labels.length > 0) {
119
  await github.rest.issues.addLabels({ ...issueParams, labels: config.labels });
120
  }
121
 
122
- // 5. 执行操作:修改状态 (Close/Reopen)
123
  if (config.state) {
124
  await github.rest.issues.update({ ...issueParams, state: config.state });
125
  }
126
 
127
- // 6. 发布执行报告
128
  await github.rest.issues.createComment({
129
  ...issueParams,
130
  body: `### 🤖 AI Assistant Action\n\n${commentBody}`
131
  });
132
 
133
  } catch (err) {
134
- console.error(err);
135
  core.setFailed(err.message);
136
  }
 
1
+ name: AI Repository Assistant (Role-Aware)
2
 
3
  on:
4
  pull_request:
 
6
  issues:
7
  types: [opened]
8
  issue_comment:
9
+ types: [created]
10
 
11
  jobs:
12
  ai-assistant:
 
13
  if: |
14
  github.event_name == 'pull_request' ||
15
  github.event_name == 'issues' ||
 
27
  - name: Prepare Context
28
  id: prep
29
  run: |
30
+ # 提取基本信息
31
+ ISSUE_AUTHOR="${{ github.event.issue.user.login || github.event.pull_request.user.login }}"
32
+ CURRENT_ACTOR="${{ github.actor }}"
33
+
34
+ echo "### METADATA ###" > content.txt
35
+ echo "Issue/PR Author: @$ISSUE_AUTHOR" >> content.txt
36
+ echo "Instruction triggered by: @$CURRENT_ACTOR" >> content.txt
37
+ echo "Assistant Identity: @github-actions[bot]" >> content.txt
38
+ echo -e "------------------\n" >> content.txt
39
+
40
+ if [ "${{ github.event_name }}" == "issue_comment" ]; then
41
+ echo "### NEW COMMAND FROM @$CURRENT_ACTOR ###" >> content.txt
42
+ echo "Comment: ${{ github.event.comment.body }}" >> content.txt
43
+ echo -e "\n### ORIGINAL CONTEXT ###" >> content.txt
44
+ echo "Title: ${{ github.event.issue.title }}" >> content.txt
45
+ echo "Description: ${{ github.event.issue.body }}" >> content.txt
46
+ elif [ "${{ github.event_name }}" == "pull_request" ]; then
47
+ echo "### NEW PULL REQUEST FROM @$ISSUE_AUTHOR ###" >> content.txt
48
  git fetch origin ${{ github.event.pull_request.base.ref }}
49
+ git diff origin/${{ github.event.pull_request.base.ref }}...HEAD > pr_diff.txt
50
+ cat pr_diff.txt >> content.txt
51
  else
52
+ echo "### NEW ISSUE FROM @$ISSUE_AUTHOR ###" >> content.txt
53
+ echo "Title: ${{ github.event.issue.title }}" >> content.txt
54
+ echo "Body: ${{ github.event.issue.body }}" >> content.txt
 
 
 
 
 
 
55
  fi
56
+
57
+ echo "number=${{ github.event.issue.number || github.event.pull_request.number }}" >> $GITHUB_OUTPUT
58
+ echo "type=${{ github.event_name }}" >> $GITHUB_OUTPUT
59
 
60
  - name: AI Action Execution
61
  uses: actions/github-script@v7
 
63
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
64
  BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
65
  MODEL: ${{ secrets.AI_MODEL }}
 
66
  ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
67
  with:
68
  script: |
69
  const fs = require('fs');
70
  const content = fs.readFileSync('content.txt', 'utf8');
71
 
 
72
  const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
73
  owner: context.repo.owner,
74
  repo: context.repo.repo,
75
  });
76
  const labelNames = repoLabels.map(l => l.name);
77
 
78
+ const prompt = `你是一个拥有自主权的仓库助手 (@github-actions[bot])。
79
+ 你需要根据提供的【METADATA】区分对话中的不同角色。
 
 
 
 
 
 
 
80
 
81
+ 角���指南:
82
+ 1. Issue/PR Author: 任务的发起者。
83
+ 2. Instruction triggered by: 当前向你下达指令的人。如果是 Author 且内容包含 "[use-ai]",说明他在请求你介入。
84
+ 3. Assistant Identity: 这是你自己。请不要审视或评价你自己的历史回复。
85
+
86
+ 你的权力:
87
+ - 根据 Issue 内容的质量自动打标签(可选:${labelNames.join(", ")})。
88
+ - 如果内容是无意义的测试、违反规范或已解决,请直接关闭它。
89
+ - 你的语气应该是果断的执行者,而不是卑微的助理。
90
+
91
+ 输出要求(严格 JSON 第一行):
92
+ {"labels": ["label_name"], "state": "closed" | "open"}
93
+ 然后另起一行说明你的处理逻辑。`;
94
 
95
  try {
96
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
 
98
  headers: { 'Authorization': `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
99
  body: JSON.stringify({
100
  model: process.env.MODEL,
101
+ messages: [{ role: "user", content: prompt + "\n\n内容如下:\n" + content }],
102
+ temperature: 0.1 // 降低随机性,增强逻辑判断
103
  })
104
  });
105
 
106
  const data = await response.json();
107
  const fullText = data.choices[0].message.content;
108
 
 
109
  const jsonMatch = fullText.match(/^\{.*?\}/);
110
  let config = { labels: [], state: "open" };
111
  let commentBody = fullText;
 
121
  issue_number: parseInt(process.env.ISSUE_NUMBER)
122
  };
123
 
124
+ // 执行打标签
125
  if (config.labels && config.labels.length > 0) {
126
  await github.rest.issues.addLabels({ ...issueParams, labels: config.labels });
127
  }
128
 
129
+ // 执行状态变更
130
  if (config.state) {
131
  await github.rest.issues.update({ ...issueParams, state: config.state });
132
  }
133
 
134
+ // 发表回复
135
  await github.rest.issues.createComment({
136
  ...issueParams,
137
  body: `### 🤖 AI Assistant Action\n\n${commentBody}`
138
  });
139
 
140
  } catch (err) {
 
141
  core.setFailed(err.message);
142
  }
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,13 +1,20 @@
1
- name: AI PR & Issue Assistant with Auto-Label
2
 
3
  on:
4
  pull_request:
5
- types: [opened, synchronize]
6
  issues:
7
  types: [opened]
 
 
8
 
9
  jobs:
10
  ai-assistant:
 
 
 
 
 
11
  runs-on: ubuntu-latest
12
  permissions:
13
  contents: read
@@ -21,18 +28,27 @@ jobs:
21
  - name: Prepare Context
22
  id: prep
23
  run: |
24
- if [ "${{ github.event_name }}" == "pull_request" ]; then
25
- git fetch origin ${{ github.base_ref }}
26
- git diff origin/${{ github.base_ref }}...HEAD > input_content.txt
27
- echo "type=PR" >> $GITHUB_OUTPUT
28
- echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
 
29
  else
30
- echo -e "Title: ${{ github.event.issue.title }}\n\n${{ github.event.issue.body }}" > input_content.txt
31
- echo "type=Issue" >> $GITHUB_OUTPUT
32
- echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
 
 
 
 
 
 
33
  fi
 
 
34
 
35
- - name: AI Processing & Labeling
36
  uses: actions/github-script@v7
37
  env:
38
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
@@ -40,42 +56,38 @@ jobs:
40
  MODEL: ${{ secrets.AI_MODEL }}
41
  EVENT_TYPE: ${{ steps.prep.outputs.type }}
42
  ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
43
- ENABLE_AUTO_LABEL: "true" # 设置为 "false" 则彻底关闭自动打标签功能
44
  with:
45
  script: |
46
  const fs = require('fs');
47
- const content = fs.readFileSync('input_content.txt', 'utf8');
48
- if (!content.trim()) return;
49
 
50
- // 1. 获取仓库现有的所有 Labels
51
  const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
52
  owner: context.repo.owner,
53
  repo: context.repo.repo,
54
  });
55
  const labelNames = repoLabels.map(l => l.name);
56
 
57
- // 2. 构建针对标签的 Prompt
58
- let labelInstruction = "";
59
- if (process.env.ENABLE_AUTO_LABEL === "true") {
60
- labelInstruction = `
61
- 请从以下现有的标签列表中选择最合适的标签(可多选),仅从列表中选择:[${labelNames.join(", ")}]。
62
- 如果你认为不需要打标签,请返回空数组。
63
- 请在回复的最开头以 JSON 格式输出你的选择,格式如下:
64
- {"suggested_labels": ["label1", "label2"]}
65
- 然后另起一行开始你的评论分析。`;
66
- }
67
-
68
- const prompt = process.env.EVENT_TYPE === 'PR'
69
- ? `你是一位资深工程师。请审查以下 PR 的代码改动,指出潜在 Bug 和改进点。${labelInstruction}\n\nDiff内容:\n${content}`
70
- : `你是一位开源项目维护者。请分析以下 Issue 并给出建议。${labelInstruction}\n\nIssue内容:\n${content}`;
 
71
 
72
  try {
73
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
74
  method: 'POST',
75
- headers: {
76
- 'Authorization': `Bearer ${process.env.API_KEY}`,
77
- 'Content-Type': 'application/json'
78
- },
79
  body: JSON.stringify({
80
  model: process.env.MODEL,
81
  messages: [{ role: "user", content: prompt }],
@@ -86,40 +98,39 @@ jobs:
86
  const data = await response.json();
87
  const fullText = data.choices[0].message.content;
88
 
89
- // 3. 解析标签和正文
 
 
90
  let commentBody = fullText;
91
- let labelsToAdd = [];
92
-
93
- if (process.env.ENABLE_AUTO_LABEL === "true") {
94
- const jsonMatch = fullText.match(/^\{.*?\}/); // 匹配开头的 JSON
95
- if (jsonMatch) {
96
- try {
97
- const labelData = JSON.parse(jsonMatch[0]);
98
- labelsToAdd = labelData.suggested_labels || [];
99
- commentBody = fullText.replace(jsonMatch[0], "").trim();
100
- } catch (e) {
101
- console.log("Failed to parse labels JSON");
102
- }
103
- }
104
  }
105
 
106
- // 4. 发布评论
107
- await github.rest.issues.createComment({
108
  owner: context.repo.owner,
109
  repo: context.repo.repo,
110
- issue_number: parseInt(process.env.ISSUE_NUMBER),
111
- body: `### 🤖 AI ${process.env.EVENT_TYPE === 'PR' ? 'Review' : 'Assistant'}\n\n${commentBody}`
112
- });
113
 
114
- // 5. 自动打标签(如果有建议的标签)
115
- if (labelsToAdd.length > 0) {
116
- await github.rest.issues.addLabels({
117
- owner: context.repo.owner,
118
- repo: context.repo.repo,
119
- issue_number: parseInt(process.env.ISSUE_NUMBER),
120
- labels: labelsToAdd
121
- });
122
  }
 
 
 
 
 
 
 
 
 
 
 
 
123
  } catch (err) {
 
124
  core.setFailed(err.message);
125
  }
 
1
+ name: AI Repository Assistant
2
 
3
  on:
4
  pull_request:
5
+ types: [opened]
6
  issues:
7
  types: [opened]
8
+ issue_comment:
9
+ types: [created] # 支持通过评论触发
10
 
11
  jobs:
12
  ai-assistant:
13
+ # 仅在:1. 新开 PR/Issue 2. 评论以 [use-ai] 开头时运行
14
+ if: |
15
+ github.event_name == 'pull_request' ||
16
+ github.event_name == 'issues' ||
17
+ startsWith(github.event.comment.body, '[use-ai]')
18
  runs-on: ubuntu-latest
19
  permissions:
20
  contents: read
 
28
  - name: Prepare Context
29
  id: prep
30
  run: |
31
+ # 判断是 PR 还是 Issue
32
+ if [ "${{ github.event.pull_request }}" != "" ]; then
33
+ EVENT_TYPE="PR"
34
+ ISSUE_NUMBER="${{ github.event.pull_request.number }}"
35
+ git fetch origin ${{ github.event.pull_request.base.ref }}
36
+ git diff origin/${{ github.event.pull_request.base.ref }}...HEAD > content.txt
37
  else
38
+ EVENT_TYPE="Issue"
39
+ ISSUE_NUMBER="${{ github.event.issue.number }}"
40
+ # 如果是评论触发,获取评论内容作为补充指令
41
+ if [ "${{ github.event_name }}" == "issue_comment" ]; then
42
+ echo "Command: ${{ github.event.comment.body }}" > content.txt
43
+ echo -e "\nOriginal Content:\n${{ github.event.issue.title }}\n${{ github.event.issue.body }}" >> content.txt
44
+ else
45
+ echo -e "Title: ${{ github.event.issue.title }}\n\n${{ github.event.issue.body }}" > content.txt
46
+ fi
47
  fi
48
+ echo "type=$EVENT_TYPE" >> $GITHUB_OUTPUT
49
+ echo "number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT
50
 
51
+ - name: AI Action Execution
52
  uses: actions/github-script@v7
53
  env:
54
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
 
56
  MODEL: ${{ secrets.AI_MODEL }}
57
  EVENT_TYPE: ${{ steps.prep.outputs.type }}
58
  ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
 
59
  with:
60
  script: |
61
  const fs = require('fs');
62
+ const content = fs.readFileSync('content.txt', 'utf8');
 
63
 
64
+ // 1. 获取现有 Labels
65
  const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
66
  owner: context.repo.owner,
67
  repo: context.repo.repo,
68
  });
69
  const labelNames = repoLabels.map(l => l.name);
70
 
71
+ // 2. 角色设定:从“建议者”转变为“执行者”
72
+ const prompt = `你是一个自动化的仓库助手。你被授权管理该仓库的 Issues 和 PR。
73
+ 你的任务:分析内容,打上标签,并决定是否关闭或保持开启。
74
+
75
+ 仓库现有标签:[${labelNames.join(", ")}]。
76
+
77
+ 输出要求:
78
+ 必须在回复的第一行输出 JSON 格式的操作指令:
79
+ {"labels": ["label1"], "state": "closed" | "open"}
80
+
81
+ 随后另起一行,以助手的身份简洁地说明你执行的操作和理由。不要说“我建议”,要说“我已执行”。
82
+
83
+ 当前环境:${process.env.EVENT_TYPE}
84
+ 内容详情:
85
+ ${content}`;
86
 
87
  try {
88
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
89
  method: 'POST',
90
+ headers: { 'Authorization': `Bearer ${process.env.API_KEY}`, 'Content-Type': 'application/json' },
 
 
 
91
  body: JSON.stringify({
92
  model: process.env.MODEL,
93
  messages: [{ role: "user", content: prompt }],
 
98
  const data = await response.json();
99
  const fullText = data.choices[0].message.content;
100
 
101
+ // 3. 解析指令
102
+ const jsonMatch = fullText.match(/^\{.*?\}/);
103
+ let config = { labels: [], state: "open" };
104
  let commentBody = fullText;
105
+
106
+ if (jsonMatch) {
107
+ config = JSON.parse(jsonMatch[0]);
108
+ commentBody = fullText.replace(jsonMatch[0], "").trim();
 
 
 
 
 
 
 
 
 
109
  }
110
 
111
+ const issueParams = {
 
112
  owner: context.repo.owner,
113
  repo: context.repo.repo,
114
+ issue_number: parseInt(process.env.ISSUE_NUMBER)
115
+ };
 
116
 
117
+ // 4. 执行操作:打标签
118
+ if (config.labels && config.labels.length > 0) {
119
+ await github.rest.issues.addLabels({ ...issueParams, labels: config.labels });
 
 
 
 
 
120
  }
121
+
122
+ // 5. 执行操作:修改状态 (Close/Reopen)
123
+ if (config.state) {
124
+ await github.rest.issues.update({ ...issueParams, state: config.state });
125
+ }
126
+
127
+ // 6. 发布执行报告
128
+ await github.rest.issues.createComment({
129
+ ...issueParams,
130
+ body: `### 🤖 AI Assistant Action\n\n${commentBody}`
131
+ });
132
+
133
  } catch (err) {
134
+ console.error(err);
135
  core.setFailed(err.message);
136
  }
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml CHANGED
@@ -1,10 +1,10 @@
1
- name: AI PR & Issue Helper
2
 
3
  on:
4
  pull_request:
5
  types: [opened, synchronize]
6
  issues:
7
- types: [opened] # 当新 Issue 创建时触发
8
 
9
  jobs:
10
  ai-assistant:
@@ -17,37 +17,57 @@ jobs:
17
  steps:
18
  - name: Checkout Code
19
  uses: actions/checkout@v4
20
- with:
21
- fetch-depth: 0
22
 
23
  - name: Prepare Context
24
  id: prep
25
  run: |
26
  if [ "${{ github.event_name }}" == "pull_request" ]; then
27
- git diff origin/${{ github.base_ref }}...origin/${{ github.head_ref }} > input_content.txt
 
28
  echo "type=PR" >> $GITHUB_OUTPUT
 
29
  else
30
- echo "${{ github.event.issue.title }}\n\n${{ github.event.issue.body }}" > input_content.txt
31
  echo "type=Issue" >> $GITHUB_OUTPUT
 
32
  fi
33
 
34
- - name: AI Processing
35
  uses: actions/github-script@v7
36
  env:
37
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
38
  BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
39
  MODEL: ${{ secrets.AI_MODEL }}
40
  EVENT_TYPE: ${{ steps.prep.outputs.type }}
 
 
41
  with:
42
  script: |
43
  const fs = require('fs');
44
  const content = fs.readFileSync('input_content.txt', 'utf8');
45
  if (!content.trim()) return;
46
 
47
- // 根据类型定制 Prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  const prompt = process.env.EVENT_TYPE === 'PR'
49
- ? `你是一位资深工程师。请审查以下 PR 的代码改动,指出潜在 Bug 和改进点:\n\n${content}`
50
- : `你是一位开源项目维护者。请分析以下 Issue 并给出建议或解决方案:\n\n${content}`;
51
 
52
  try {
53
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
@@ -59,27 +79,45 @@ jobs:
59
  body: JSON.stringify({
60
  model: process.env.MODEL,
61
  messages: [{ role: "user", content: prompt }],
62
- temperature: 0.3
63
  })
64
  });
65
 
66
  const data = await response.json();
67
- const result = data.choices[0].message.content;
68
- const header = process.env.EVENT_TYPE === 'PR' ? "### 🤖 AI Code Review" : "### 🤖 AI Issue Assistant";
69
 
70
- if (process.env.EVENT_TYPE === 'PR') {
71
- await github.rest.issues.createComment({
72
- owner: context.repo.owner,
73
- repo: context.repo.repo,
74
- issue_number: context.payload.pull_request.number,
75
- body: `${header}\n\n${result}`
76
- });
77
- } else {
78
- await github.rest.issues.createComment({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  owner: context.repo.owner,
80
  repo: context.repo.repo,
81
- issue_number: context.payload.issue.number,
82
- body: `${header}\n\n${result}`
83
  });
84
  }
85
  } catch (err) {
 
1
+ name: AI PR & Issue Assistant with Auto-Label
2
 
3
  on:
4
  pull_request:
5
  types: [opened, synchronize]
6
  issues:
7
+ types: [opened]
8
 
9
  jobs:
10
  ai-assistant:
 
17
  steps:
18
  - name: Checkout Code
19
  uses: actions/checkout@v4
 
 
20
 
21
  - name: Prepare Context
22
  id: prep
23
  run: |
24
  if [ "${{ github.event_name }}" == "pull_request" ]; then
25
+ git fetch origin ${{ github.base_ref }}
26
+ git diff origin/${{ github.base_ref }}...HEAD > input_content.txt
27
  echo "type=PR" >> $GITHUB_OUTPUT
28
+ echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
29
  else
30
+ echo -e "Title: ${{ github.event.issue.title }}\n\n${{ github.event.issue.body }}" > input_content.txt
31
  echo "type=Issue" >> $GITHUB_OUTPUT
32
+ echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT
33
  fi
34
 
35
+ - name: AI Processing & Labeling
36
  uses: actions/github-script@v7
37
  env:
38
  API_KEY: ${{ secrets.OPENAI_API_KEY }}
39
  BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
40
  MODEL: ${{ secrets.AI_MODEL }}
41
  EVENT_TYPE: ${{ steps.prep.outputs.type }}
42
+ ISSUE_NUMBER: ${{ steps.prep.outputs.number }}
43
+ ENABLE_AUTO_LABEL: "true" # 设置为 "false" 则彻底关闭自动打标签功能
44
  with:
45
  script: |
46
  const fs = require('fs');
47
  const content = fs.readFileSync('input_content.txt', 'utf8');
48
  if (!content.trim()) return;
49
 
50
+ // 1. 获取仓库现有的所有 Labels
51
+ const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
52
+ owner: context.repo.owner,
53
+ repo: context.repo.repo,
54
+ });
55
+ const labelNames = repoLabels.map(l => l.name);
56
+
57
+ // 2. 构建针对标签的 Prompt
58
+ let labelInstruction = "";
59
+ if (process.env.ENABLE_AUTO_LABEL === "true") {
60
+ labelInstruction = `
61
+ 请从以下现有的标签列表中选择最合适的标签(可多选),仅从列表中选择:[${labelNames.join(", ")}]。
62
+ 如果你认为不需要打标签,请返回空数组。
63
+ 请在回复的最开头以 JSON 格式输出你的选择,格式如下:
64
+ {"suggested_labels": ["label1", "label2"]}
65
+ 然后另起一行开始你的评论分析。`;
66
+ }
67
+
68
  const prompt = process.env.EVENT_TYPE === 'PR'
69
+ ? `你是一位资深工程师。请审查以下 PR 的代码改动,指出潜在 Bug 和改进点。${labelInstruction}\n\nDiff内容:\n${content}`
70
+ : `你是一位开源项目维护者。请分析以下 Issue 并给出建议。${labelInstruction}\n\nIssue内容:\n${content}`;
71
 
72
  try {
73
  const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
 
79
  body: JSON.stringify({
80
  model: process.env.MODEL,
81
  messages: [{ role: "user", content: prompt }],
82
+ temperature: 0.2
83
  })
84
  });
85
 
86
  const data = await response.json();
87
+ const fullText = data.choices[0].message.content;
 
88
 
89
+ // 3. 解析标签和正文
90
+ let commentBody = fullText;
91
+ let labelsToAdd = [];
92
+
93
+ if (process.env.ENABLE_AUTO_LABEL === "true") {
94
+ const jsonMatch = fullText.match(/^\{.*?\}/); // 匹配开头的 JSON
95
+ if (jsonMatch) {
96
+ try {
97
+ const labelData = JSON.parse(jsonMatch[0]);
98
+ labelsToAdd = labelData.suggested_labels || [];
99
+ commentBody = fullText.replace(jsonMatch[0], "").trim();
100
+ } catch (e) {
101
+ console.log("Failed to parse labels JSON");
102
+ }
103
+ }
104
+ }
105
+
106
+ // 4. 发布评论
107
+ await github.rest.issues.createComment({
108
+ owner: context.repo.owner,
109
+ repo: context.repo.repo,
110
+ issue_number: parseInt(process.env.ISSUE_NUMBER),
111
+ body: `### 🤖 AI ${process.env.EVENT_TYPE === 'PR' ? 'Review' : 'Assistant'}\n\n${commentBody}`
112
+ });
113
+
114
+ // 5. 自动打标签(如果有建议的标签)
115
+ if (labelsToAdd.length > 0) {
116
+ await github.rest.issues.addLabels({
117
  owner: context.repo.owner,
118
  repo: context.repo.repo,
119
+ issue_number: parseInt(process.env.ISSUE_NUMBER),
120
+ labels: labelsToAdd
121
  });
122
  }
123
  } catch (err) {
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: AI PR & Issue Helper
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize]
6
+ issues:
7
+ types: [opened] # 当新 Issue 创建时触发
8
+
9
+ jobs:
10
+ ai-assistant:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: read
14
+ pull-requests: write
15
+ issues: write
16
+
17
+ steps:
18
+ - name: Checkout Code
19
+ uses: actions/checkout@v4
20
+ with:
21
+ fetch-depth: 0
22
+
23
+ - name: Prepare Context
24
+ id: prep
25
+ run: |
26
+ if [ "${{ github.event_name }}" == "pull_request" ]; then
27
+ git diff origin/${{ github.base_ref }}...origin/${{ github.head_ref }} > input_content.txt
28
+ echo "type=PR" >> $GITHUB_OUTPUT
29
+ else
30
+ echo "${{ github.event.issue.title }}\n\n${{ github.event.issue.body }}" > input_content.txt
31
+ echo "type=Issue" >> $GITHUB_OUTPUT
32
+ fi
33
+
34
+ - name: AI Processing
35
+ uses: actions/github-script@v7
36
+ env:
37
+ API_KEY: ${{ secrets.OPENAI_API_KEY }}
38
+ BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
39
+ MODEL: ${{ secrets.AI_MODEL }}
40
+ EVENT_TYPE: ${{ steps.prep.outputs.type }}
41
+ with:
42
+ script: |
43
+ const fs = require('fs');
44
+ const content = fs.readFileSync('input_content.txt', 'utf8');
45
+ if (!content.trim()) return;
46
+
47
+ // 根据类型定制 Prompt
48
+ const prompt = process.env.EVENT_TYPE === 'PR'
49
+ ? `你是一位资深工程师。请审查以下 PR 的代码改动,指出潜在 Bug 和改进点:\n\n${content}`
50
+ : `你是一位开源项目维护者。请分析以下 Issue 并给出建议或解决方案:\n\n${content}`;
51
+
52
+ try {
53
+ const response = await fetch(`${process.env.BASE_URL}/chat/completions`, {
54
+ method: 'POST',
55
+ headers: {
56
+ 'Authorization': `Bearer ${process.env.API_KEY}`,
57
+ 'Content-Type': 'application/json'
58
+ },
59
+ body: JSON.stringify({
60
+ model: process.env.MODEL,
61
+ messages: [{ role: "user", content: prompt }],
62
+ temperature: 0.3
63
+ })
64
+ });
65
+
66
+ const data = await response.json();
67
+ const result = data.choices[0].message.content;
68
+ const header = process.env.EVENT_TYPE === 'PR' ? "### 🤖 AI Code Review" : "### 🤖 AI Issue Assistant";
69
+
70
+ if (process.env.EVENT_TYPE === 'PR') {
71
+ await github.rest.issues.createComment({
72
+ owner: context.repo.owner,
73
+ repo: context.repo.repo,
74
+ issue_number: context.payload.pull_request.number,
75
+ body: `${header}\n\n${result}`
76
+ });
77
+ } else {
78
+ await github.rest.issues.createComment({
79
+ owner: context.repo.owner,
80
+ repo: context.repo.repo,
81
+ issue_number: context.payload.issue.number,
82
+ body: `${header}\n\n${result}`
83
+ });
84
+ }
85
+ } catch (err) {
86
+ core.setFailed(err.message);
87
+ }
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/push_hf.yaml ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face (Exclude README)
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ sync-to-hub:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - name: Checkout GitHub Repository
13
+ uses: actions/checkout@v4
14
+ with:
15
+ fetch-depth: 0
16
+ lfs: true
17
+
18
+ - name: Sync and Push to HF
19
+ env:
20
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
21
+ run: |
22
+ # 1. 配置 Git 用户信息
23
+ git config --global user.email "actions@github.com"
24
+ git config --global user.name "GitHub Actions"
25
+
26
+ # 2. 克隆 Hugging Face 仓库到临时目录 'hf_repo'
27
+ # 注意:请将 <USERNAME>/<REPO_NAME> 替换为你的路径
28
+ git clone https://x-access-token:$HF_TOKEN@huggingface.co/spaces/StarrySkyWorld/InterConnectServer hf_repo
29
+
30
+ # 3. 使用 rsync 同步文件
31
+ # --exclude='.git/' : 不同步 git 历史
32
+ # --exclude='README.md' : 不同步 README 文件
33
+ # -av : 归档模式并显示详细过程
34
+ # --delete : 如果 GitHub 删除了某文件,HF 端也相应删除(README 除外)
35
+ rsync -av --exclude='.git/' --exclude='README.md' ./ hf_repo/
36
+
37
+ # 4. 进入临时目录提交并推送
38
+ cd hf_repo
39
+ git add .
40
+
41
+ # 检查是否有内容变化,防止空提交导致报错
42
+ if [ -n "$(git status --porcelain)" ]; then
43
+ git commit -m "Sync from GitHub (excluding README)"
44
+ git push origin main
45
+ else
46
+ echo "No changes to sync."
47
+ fi
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.gitignore ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Environment variables
5
+ .env
6
+ .env.local
7
+ .env.*.local
8
+
9
+ # Database files
10
+ *.db
11
+ *.sqlite
12
+ *.sqlite3
13
+
14
+ # Logs
15
+ npm-debug.log*
16
+ yarn-debug.log*
17
+ yarn-error.log*
18
+ pnpm-debug.log*
19
+ lerna-debug.log*
20
+
21
+ # Build files
22
+ dist/
23
+ build/
24
+
25
+ # IDE
26
+ .idea/
27
+ .vscode/
28
+ *.swp
29
+ *.swo
30
+ *~
31
+ .trae/
32
+
33
+ # OS
34
+ .DS_Store
35
+ Thumbs.db
36
+
37
+ # Testing
38
+ coverage/
39
+ .nyc_output/
40
+
41
+ # Misc
42
+ *.tgz
43
+ .cache/
44
+ .parcel-cache/
45
+ .next/
46
+ .nuxt/
47
+ .vuepress/dist/
48
+ .serverless/
49
+ .fusebox/
50
+ .dynamodb/
51
+
52
+ # CLI
53
+ cli/*.log
54
+
55
+ # Docker
56
+ .dockerignore
57
+ *.dockerfile
58
+
59
+
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package*.json ./
6
+
7
+ RUN npm install --production
8
+
9
+ COPY . .
10
+
11
+ EXPOSE 8000
12
+
13
+ ENV SERVER_HOST=0.0.0.0
14
+ ENV SERVER_PORT=8000
15
+
16
+ CMD ["node", "src/server.js"]
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BeiChen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/cli/cli.js ADDED
@@ -0,0 +1,389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+ const { Command } = require('commander');
3
+ const http = require('http');
4
+ const https = require('https');
5
+ const fs = require('fs');
6
+ require('dotenv').config();
7
+
8
+ const HARDCODED_API_URL = 'http://localhost:8000';
9
+ const HARDCODED_ADMIN_KEY = process.env.ADMIN_KEY || null;
10
+
11
+ class MinecraftWSCLIClient {
12
+ constructor(serverUrl, adminKey) {
13
+ this.serverUrl = serverUrl.replace(/\/$/, '');
14
+ this.effectiveAdminKey = adminKey || HARDCODED_ADMIN_KEY;
15
+ }
16
+
17
+ async request(method, endpoint, data = null) {
18
+ return new Promise((resolve, reject) => {
19
+ const url = new URL(endpoint, this.serverUrl);
20
+ const isHttps = url.protocol === 'https:';
21
+ const lib = isHttps ? https : http;
22
+
23
+ const options = {
24
+ hostname: url.hostname,
25
+ port: url.port || (isHttps ? 443 : 80),
26
+ path: url.pathname + url.search,
27
+ method: method,
28
+ headers: {
29
+ 'Content-Type': 'application/json'
30
+ }
31
+ };
32
+
33
+ if (this.effectiveAdminKey) {
34
+ options.headers['Authorization'] = `Bearer ${this.effectiveAdminKey}`;
35
+ }
36
+
37
+ const req = lib.request(options, (res) => {
38
+ let body = '';
39
+ res.on('data', chunk => body += chunk);
40
+ res.on('end', () => {
41
+ if (res.statusCode >= 200 && res.statusCode < 300) {
42
+ if (res.statusCode === 204 || !body) {
43
+ resolve(null);
44
+ } else {
45
+ try {
46
+ resolve(JSON.parse(body));
47
+ } catch {
48
+ resolve(body);
49
+ }
50
+ }
51
+ } else {
52
+ let detail = body;
53
+ try {
54
+ const json = JSON.parse(body);
55
+ detail = json.detail || body;
56
+ } catch {}
57
+ reject(new Error(`API请求失败 (${res.statusCode}): ${detail}`));
58
+ }
59
+ });
60
+ });
61
+
62
+ req.on('error', (e) => reject(new Error(`网络请求失败: ${e.message}`)));
63
+
64
+ if (data) {
65
+ req.write(JSON.stringify(data));
66
+ }
67
+ req.end();
68
+ });
69
+ }
70
+
71
+ ensureAdminKeyForManagement() {
72
+ if (!this.effectiveAdminKey) {
73
+ throw new Error('此管理操作需要Admin Key。请通过--admin-key选项或ADMIN_KEY环境变量提供。');
74
+ }
75
+ }
76
+
77
+ async createApiKey(name, description = '', keyType = 'regular', serverId = null) {
78
+ this.ensureAdminKeyForManagement();
79
+ return await this.request('POST', '/manage/keys', {
80
+ name,
81
+ description,
82
+ key_type: keyType,
83
+ server_id: serverId
84
+ });
85
+ }
86
+
87
+ async listApiKeys() {
88
+ this.ensureAdminKeyForManagement();
89
+ return await this.request('GET', '/manage/keys');
90
+ }
91
+
92
+ async getApiKeyDetails(keyId) {
93
+ this.ensureAdminKeyForManagement();
94
+ return await this.request('GET', `/manage/keys/${keyId}`);
95
+ }
96
+
97
+ async activateApiKey(keyId) {
98
+ this.ensureAdminKeyForManagement();
99
+ return await this.request('PATCH', `/manage/keys/${keyId}/activate`);
100
+ }
101
+
102
+ async deactivateApiKey(keyId) {
103
+ this.ensureAdminKeyForManagement();
104
+ return await this.request('PATCH', `/manage/keys/${keyId}/deactivate`);
105
+ }
106
+
107
+ async deleteApiKey(keyId) {
108
+ this.ensureAdminKeyForManagement();
109
+ await this.request('DELETE', `/manage/keys/${keyId}`);
110
+ return true;
111
+ }
112
+
113
+ async healthCheck() {
114
+ return await this.request('GET', '/health');
115
+ }
116
+ }
117
+
118
+ const program = new Command();
119
+
120
+ program
121
+ .name('minecraft-ws-cli')
122
+ .description('Minecraft WebSocket API CLI - 管理API密钥和服务器')
123
+ .version('1.0.0')
124
+ .option('-s, --server-url <url>', 'API服务器URL', process.env.MC_WS_API_URL || HARDCODED_API_URL)
125
+ .option('-k, --admin-key <key>', '用于管理操作的Admin Key', process.env.ADMIN_KEY || HARDCODED_ADMIN_KEY);
126
+
127
+ program
128
+ .command('create-key <name>')
129
+ .description('创建新的API密钥')
130
+ .option('-d, --description <desc>', 'API密钥描述', '')
131
+ .option('-t, --type <type>', '密钥类型: admin, server, regular', 'regular')
132
+ .option('--server-id <id>', '关联的服务器ID(仅server类型需要)')
133
+ .action(async (name, options) => {
134
+ try {
135
+ const opts = program.opts();
136
+ const client = new MinecraftWSCLIClient(opts.serverUrl, opts.adminKey);
137
+ const result = await client.createApiKey(name, options.description, options.type, options.serverId);
138
+
139
+ const keyTypeNames = { admin: 'Admin Key', server: 'Server Key', regular: '普通Key' };
140
+
141
+ if (result.regularKey && result.serverKey) {
142
+ // 返回了Regular Key和关联的Server Key
143
+ console.log(`\x1b[32m✅ 成功创建Regular Key和关联的Server Key!\x1b[0m`);
144
+ console.log('='.repeat(60));
145
+
146
+ // 显示Regular Key
147
+ console.log(`\x1b[34m🔑 Regular Key:${keyTypeNames[result.regularKey.keyType]}\x1b[0m`);
148
+ console.log(` ID: ${result.regularKey.id}`);
149
+ console.log(` 名称: ${result.regularKey.name}`);
150
+ console.log(` 描述: ${result.regularKey.description || '无'}`);
151
+ console.log(` 前缀: ${result.regularKey.keyPrefix}`);
152
+ if (result.regularKey.serverId) {
153
+ console.log(` 服务器ID: ${result.regularKey.serverId}`);
154
+ }
155
+ console.log(` 创建时间: ${result.regularKey.createdAt}`);
156
+ console.log(`\x1b[33m🔑 原始密钥 (请妥善保存,仅显示一次):\x1b[0m`);
157
+ console.log(`\x1b[31m ${result.regularKey.key}\x1b[0m`);
158
+ console.log();
159
+
160
+ // 显示关联的Server Key
161
+ console.log(`\x1b[34m🖥️ Server Key:${keyTypeNames[result.serverKey.keyType]}\x1b[0m`);
162
+ console.log(` ID: ${result.serverKey.id}`);
163
+ console.log(` 名称: ${result.serverKey.name}`);
164
+ console.log(` 描述: ${result.serverKey.description || '无'}`);
165
+ console.log(` 前缀: ${result.serverKey.keyPrefix}`);
166
+ if (result.serverKey.serverId) {
167
+ console.log(` 服务器ID: ${result.serverKey.serverId}`);
168
+ }
169
+ console.log(` 创建时间: ${result.serverKey.createdAt}`);
170
+ console.log(`\x1b[33m🔑 原始密钥 (请妥善保存,仅显示一次):\x1b[0m`);
171
+ console.log(`\x1b[31m ${result.serverKey.key}\x1b[0m`);
172
+ console.log('='.repeat(60));
173
+ console.log();
174
+ console.log('使用示例:');
175
+ console.log(` Regular Key登录控制面板: http://localhost:8000/dashboard`);
176
+ console.log(` Server Key用于插件配置: 放入Minecraft插件的配置文件中`);
177
+ } else {
178
+ // 返回了单个密钥
179
+ const keyType = keyTypeNames[result.keyType] || result.keyType;
180
+
181
+ console.log(`\x1b[32m✅ ${keyType}创建成功!\x1b[0m`);
182
+ console.log(` ID: ${result.id}`);
183
+ console.log(` 名称: ${result.name}`);
184
+ console.log(` 类型: ${keyType}`);
185
+ console.log(` 前缀: ${result.keyPrefix}`);
186
+ if (result.serverId) {
187
+ console.log(` 服务器ID: ${result.serverId}`);
188
+ }
189
+ console.log(` 创建时间: ${result.createdAt}`);
190
+ console.log(`\x1b[33m🔑 原始密钥 (请妥善保存,仅显示一次):\x1b[0m`);
191
+ console.log(`\x1b[31m ${result.key}\x1b[0m`);
192
+ console.log();
193
+ console.log('使用示例:');
194
+ console.log(` WebSocket连接: ws://localhost:8000/ws?api_key=${result.key}`);
195
+ console.log(` HTTP请求头: Authorization: Bearer ${result.key}`);
196
+ }
197
+ } catch (error) {
198
+ console.error(`\x1b[31m❌ 错误: ${error.message}\x1b[0m`);
199
+ process.exit(1);
200
+ }
201
+ });
202
+
203
+ program
204
+ .command('list-keys')
205
+ .description('列出所有API密钥')
206
+ .action(async () => {
207
+ try {
208
+ const opts = program.opts();
209
+ const client = new MinecraftWSCLIClient(opts.serverUrl, opts.adminKey);
210
+ const keys = await client.listApiKeys();
211
+
212
+ if (keys.length === 0) {
213
+ console.log('📭 没有找到API密钥.');
214
+ return;
215
+ }
216
+
217
+ console.log(`\x1b[34m📋 API密钥列表 (共 ${keys.length} 个):\x1b[0m`);
218
+
219
+ const keyTypeIcons = { admin: '👑', server: '🖥️', regular: '🔑' };
220
+ const keyTypeNames = { admin: 'Admin', server: 'Server', regular: 'Regular' };
221
+
222
+ for (const key of keys) {
223
+ const status = key.isActive ? '\x1b[32m🟢 活跃\x1b[0m' : '\x1b[31m🔴 已停用\x1b[0m';
224
+ const icon = keyTypeIcons[key.keyType] || '🔑';
225
+ const typeName = keyTypeNames[key.keyType] || key.keyType;
226
+ const lastUsed = key.lastUsed || '从未使用';
227
+
228
+ console.log('-'.repeat(50));
229
+ console.log(` ID : ${key.id}`);
230
+ console.log(` 名称 : ${key.name}`);
231
+ console.log(` 类型 : ${icon} ${typeName}`);
232
+ console.log(` 状态 : ${status}`);
233
+ console.log(` 前缀 : ${key.keyPrefix}`);
234
+ if (key.serverId) {
235
+ console.log(` 服务器ID : ${key.serverId}`);
236
+ }
237
+ console.log(` 创建时间 : ${key.createdAt}`);
238
+ console.log(` 最后使用 : ${lastUsed}`);
239
+ }
240
+ console.log('-'.repeat(50));
241
+ } catch (error) {
242
+ console.error(`\x1b[31m❌ 错误: ${error.message}\x1b[0m`);
243
+ process.exit(1);
244
+ }
245
+ });
246
+
247
+ program
248
+ .command('get-key <key_id>')
249
+ .description('获取特定API密钥的详细信息')
250
+ .action(async (keyId) => {
251
+ try {
252
+ const opts = program.opts();
253
+ const client = new MinecraftWSCLIClient(opts.serverUrl, opts.adminKey);
254
+ const key = await client.getApiKeyDetails(keyId);
255
+
256
+ const keyTypeIcons = { admin: '👑', server: '🖥️', regular: '🔑' };
257
+ const keyTypeNames = { admin: 'Admin', server: 'Server', regular: 'Regular' };
258
+
259
+ const status = key.isActive ? '\x1b[32m🟢 活跃\x1b[0m' : '\x1b[31m🔴 已停用\x1b[0m';
260
+ const icon = keyTypeIcons[key.keyType] || '🔑';
261
+ const typeName = keyTypeNames[key.keyType] || key.keyType;
262
+ const lastUsed = key.lastUsed || '从未使用';
263
+
264
+ console.log(`\x1b[34m📄 密钥详情 (ID: ${key.id}):\x1b[0m`);
265
+ console.log(` 名称 : ${key.name}`);
266
+ console.log(` 描述 : ${key.description || '无'}`);
267
+ console.log(` 类型 : ${icon} ${typeName}`);
268
+ console.log(` 状态 : ${status}`);
269
+ console.log(` 前缀 : ${key.keyPrefix}`);
270
+ if (key.serverId) {
271
+ console.log(` 服务器ID : ${key.serverId}`);
272
+ }
273
+ console.log(` 创建时间 : ${key.createdAt}`);
274
+ console.log(` 最后使用 : ${lastUsed}`);
275
+ } catch (error) {
276
+ console.error(`\x1b[31m❌ 错误: ${error.message}\x1b[0m`);
277
+ process.exit(1);
278
+ }
279
+ });
280
+
281
+ program
282
+ .command('activate-key <key_id>')
283
+ .description('激活指定的API密钥')
284
+ .action(async (keyId) => {
285
+ try {
286
+ const opts = program.opts();
287
+ const client = new MinecraftWSCLIClient(opts.serverUrl, opts.adminKey);
288
+ const result = await client.activateApiKey(keyId);
289
+ console.log(`\x1b[32m✅ ${result.message}\x1b[0m`);
290
+ } catch (error) {
291
+ console.error(`\x1b[31m❌ 错误: ${error.message}\x1b[0m`);
292
+ process.exit(1);
293
+ }
294
+ });
295
+
296
+ program
297
+ .command('deactivate-key <key_id>')
298
+ .description('停用指定的API密钥')
299
+ .action(async (keyId) => {
300
+ try {
301
+ const opts = program.opts();
302
+ const client = new MinecraftWSCLIClient(opts.serverUrl, opts.adminKey);
303
+ const result = await client.deactivateApiKey(keyId);
304
+ console.log(`\x1b[32m✅ ${result.message}\x1b[0m`);
305
+ } catch (error) {
306
+ console.error(`\x1b[31m❌ 错误: ${error.message}\x1b[0m`);
307
+ process.exit(1);
308
+ }
309
+ });
310
+
311
+ program
312
+ .command('delete-key <key_id>')
313
+ .description('永久删除指定的API密钥')
314
+ .action(async (keyId) => {
315
+ try {
316
+ const opts = program.opts();
317
+ const client = new MinecraftWSCLIClient(opts.serverUrl, opts.adminKey);
318
+ await client.deleteApiKey(keyId);
319
+ console.log(`\x1b[32m✅ API密钥 ${keyId} 已成功删除!\x1b[0m`);
320
+ } catch (error) {
321
+ console.error(`\x1b[31m❌ 错误: ${error.message}\x1b[0m`);
322
+ process.exit(1);
323
+ }
324
+ });
325
+
326
+ program
327
+ .command('health')
328
+ .description('检查服务器健康状态')
329
+ .action(async () => {
330
+ try {
331
+ const opts = program.opts();
332
+ const client = new MinecraftWSCLIClient(opts.serverUrl, opts.adminKey);
333
+ const result = await client.healthCheck();
334
+
335
+ console.log('\x1b[34m🏥 服务器健康状态:\x1b[0m');
336
+ const statusIcon = result.status === 'healthy' ? '🟢' : '🔴';
337
+ console.log(` 状态 : ${statusIcon} ${result.status}`);
338
+ console.log(` 时间戳 : ${result.timestamp}`);
339
+ console.log(` 活跃WS连接数 : ${result.active_ws}`);
340
+ console.log(` 总密钥数 : ${result.keys_total}`);
341
+ console.log(` 活跃Admin Keys: ${result.admin_active}`);
342
+ console.log(` 活跃Server Keys: ${result.server_active}`);
343
+ console.log(` 活跃Regular Keys: ${result.regular_active}`);
344
+
345
+ if (result.status === 'healthy') {
346
+ console.log('\x1b[32m✅ 服务器运行正常\x1b[0m');
347
+ } else {
348
+ console.log('\x1b[33m⚠️ 服务器状态异常\x1b[0m');
349
+ }
350
+ } catch (error) {
351
+ console.error(`\x1b[31m❌ 服务器连接或检查失败: ${error.message}\x1b[0m`);
352
+ process.exit(1);
353
+ }
354
+ });
355
+
356
+ program
357
+ .command('generate-config [filename]')
358
+ .description('为Minecraft插件生成配置文件模板')
359
+ .action(async (filename = 'mc_ws_plugin_config.json') => {
360
+ try {
361
+ const opts = program.opts();
362
+ const wsUrl = opts.serverUrl.replace('http://', 'ws://').replace('https://', 'wss://');
363
+
364
+ const configTemplate = {
365
+ websocket_settings: {
366
+ server_address: `${wsUrl}/ws`,
367
+ reconnect_delay_seconds: 10,
368
+ ping_interval_seconds: 30
369
+ },
370
+ api_key: 'PASTE_YOUR_GENERATED_API_KEY_HERE',
371
+ server_identifier: 'MyMinecraftServer_1',
372
+ log_level: 'INFO',
373
+ enabled_events: {
374
+ player_join: true,
375
+ player_quit: true,
376
+ player_chat: false,
377
+ player_death: true
378
+ }
379
+ };
380
+
381
+ fs.writeFileSync(filename, JSON.stringify(configTemplate, null, 4));
382
+ console.log(`\x1b[32m✅ 插件配置文件模板已生成: ${filename}\x1b[0m`);
383
+ } catch (error) {
384
+ console.error(`\x1b[31m❌ 生成配置文件失败: ${error.message}\x1b[0m`);
385
+ process.exit(1);
386
+ }
387
+ });
388
+
389
+ program.parse();
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/app.js ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const API_URL = window.location.origin;
2
+ let superKey = null;
3
+ let ws = null;
4
+
5
+ const loginScreen = document.getElementById('login-screen');
6
+ const dashboardScreen = document.getElementById('dashboard-screen');
7
+ const loginForm = document.getElementById('login-form');
8
+ const loginError = document.getElementById('login-error');
9
+ const logoutBtn = document.getElementById('logout-btn');
10
+ const createKeyBtn = document.getElementById('create-key-btn');
11
+ const createKeyModal = document.getElementById('create-key-modal');
12
+ const createKeyForm = document.getElementById('create-key-form');
13
+ const cancelCreateBtn = document.getElementById('cancel-create-btn');
14
+ const keyDetailsModal = document.getElementById('key-details-modal');
15
+ const closeDetailsBtn = document.getElementById('close-details-btn');
16
+
17
+ loginForm.addEventListener('submit', async (e) => {
18
+ e.preventDefault();
19
+ const key = document.getElementById('super-key-input').value;
20
+
21
+ try {
22
+ // 验证密钥是否有效
23
+ const response = await fetch(`${API_URL}/health`, {
24
+ headers: { 'Authorization': `Bearer ${key}` }
25
+ });
26
+
27
+ if (response.ok) {
28
+ superKey = key;
29
+
30
+ // 验证密钥类型
31
+ let userKeyType = null;
32
+ let userServerId = null;
33
+
34
+ // 尝试Admin Key验证
35
+ const adminResponse = await fetch(`${API_URL}/manage/keys`, {
36
+ headers: { 'Authorization': `Bearer ${key}` }
37
+ });
38
+
39
+ if (adminResponse.ok) {
40
+ userKeyType = 'admin';
41
+ } else {
42
+ // 尝试Server Key验证
43
+ const serverResponse = await fetch(`${API_URL}/api/server/info`, {
44
+ headers: { 'Authorization': `Bearer ${key}` }
45
+ });
46
+
47
+ if (serverResponse.ok) {
48
+ userKeyType = 'server';
49
+ const serverData = await serverResponse.json();
50
+ userServerId = serverData.server_id;
51
+ } else {
52
+ // 尝试Regular Key验证(获取自己的Server Key列表)
53
+ const regularResponse = await fetch(`${API_URL}/manage/keys/server-keys`, {
54
+ headers: { 'Authorization': `Bearer ${key}` }
55
+ });
56
+
57
+ if (regularResponse.ok) {
58
+ userKeyType = 'regular';
59
+ } else {
60
+ throw new Error('无效的密钥或权限不足');
61
+ }
62
+ }
63
+ }
64
+
65
+ loginError.textContent = '';
66
+ showDashboard(userKeyType, userServerId);
67
+ } else {
68
+ loginError.textContent = '无效的密钥';
69
+ }
70
+ } catch (error) {
71
+ loginError.textContent = error.message || '无法连接到服务器';
72
+ }
73
+ });
74
+
75
+ logoutBtn.addEventListener('click', () => {
76
+ superKey = null;
77
+ if (ws) {
78
+ ws.close();
79
+ ws = null;
80
+ }
81
+ loginScreen.classList.remove('hidden');
82
+ dashboardScreen.classList.add('hidden');
83
+ });
84
+
85
+ createKeyBtn.addEventListener('click', () => {
86
+ createKeyModal.classList.remove('hidden');
87
+ });
88
+
89
+ cancelCreateBtn.addEventListener('click', () => {
90
+ createKeyModal.classList.add('hidden');
91
+ createKeyForm.reset();
92
+ });
93
+
94
+ closeDetailsBtn.addEventListener('click', () => {
95
+ keyDetailsModal.classList.add('hidden');
96
+ });
97
+
98
+ createKeyForm.addEventListener('submit', async (e) => {
99
+ e.preventDefault();
100
+
101
+ const name = document.getElementById('key-name').value;
102
+ const description = document.getElementById('key-description').value;
103
+ const isSuper = document.getElementById('key-is-super').checked;
104
+
105
+ try {
106
+ const response = await fetch(`${API_URL}/manage/keys`, {
107
+ method: 'POST',
108
+ headers: {
109
+ 'Authorization': `Bearer ${superKey}`,
110
+ 'Content-Type': 'application/json'
111
+ },
112
+ body: JSON.stringify({
113
+ name,
114
+ description,
115
+ is_super_key: isSuper
116
+ })
117
+ });
118
+
119
+ if (response.ok) {
120
+ const result = await response.json();
121
+ createKeyModal.classList.add('hidden');
122
+ createKeyForm.reset();
123
+
124
+ showKeyCreatedModal(result);
125
+ loadKeys();
126
+ } else {
127
+ const error = await response.json();
128
+ alert('创建失败: ' + error.detail);
129
+ }
130
+ } catch (error) {
131
+ alert('创建失败: ' + error.message);
132
+ }
133
+ });
134
+
135
+ function showKeyCreatedModal(keyData) {
136
+ const content = `
137
+ <p><strong>密钥创建成功!</strong></p>
138
+ <p>名称: ${keyData.name}</p>
139
+ <p>类型: ${keyData.isSuperKey ? 'SuperKey' : '普通密钥'}</p>
140
+ <div class="key-display">
141
+ <strong>⚠️ 请立即复制并保存此密钥(仅显示一次):</strong><br>
142
+ ${keyData.key}
143
+ </div>
144
+ `;
145
+
146
+ document.getElementById('key-details-content').innerHTML = content;
147
+ keyDetailsModal.classList.remove('hidden');
148
+ }
149
+
150
+ // 全局变量
151
+ let currentUserKeyType = null;
152
+ let currentUserServerId = null;
153
+
154
+ async function showDashboard(userKeyType, userServerId) {
155
+ currentUserKeyType = userKeyType;
156
+ currentUserServerId = userServerId;
157
+
158
+ loginScreen.classList.add('hidden');
159
+ dashboardScreen.classList.remove('hidden');
160
+
161
+ // 显示用户信息
162
+ const userInfo = document.getElementById('user-info');
163
+ const keyTypeIcons = { admin: '👑', server: '🖥️', regular: '🔑' };
164
+ const keyTypeNames = { admin: 'Admin', server: 'Server', regular: 'Regular' };
165
+ userInfo.textContent = `${keyTypeIcons[userKeyType]} ${keyTypeNames[userKeyType]} 用户`;
166
+
167
+ // 根据权限显示/隐藏功能
168
+ const adminSection = document.getElementById('admin-section');
169
+ const serverSection = document.getElementById('server-section');
170
+
171
+ if (userKeyType === 'admin') {
172
+ adminSection.style.display = 'block';
173
+ serverSection.style.display = 'block';
174
+ loadKeys();
175
+ } else if (userKeyType === 'server') {
176
+ adminSection.style.display = 'none';
177
+ serverSection.style.display = 'block';
178
+ loadServerInfo();
179
+ } else if (userKeyType === 'regular') {
180
+ adminSection.style.display = 'none';
181
+ serverSection.style.display = 'block';
182
+ loadRegularServerKeys();
183
+ }
184
+
185
+ // 根据用户类型控制创建密钥按钮的显示
186
+ const createKeyBtn = document.getElementById('create-key-btn');
187
+ if (createKeyBtn) {
188
+ createKeyBtn.style.display = userKeyType === 'admin' ? 'block' : 'none';
189
+ }
190
+
191
+ loadStats();
192
+ connectWebSocket();
193
+
194
+ setInterval(loadStats, 5000);
195
+ }
196
+
197
+ async function loadStats() {
198
+ try {
199
+ const response = await fetch(`${API_URL}/health`);
200
+ const data = await response.json();
201
+
202
+ document.getElementById('stat-connections').textContent = data.active_ws || 0;
203
+ document.getElementById('stat-total-keys').textContent = data.keys_total || 0;
204
+ document.getElementById('stat-super-keys').textContent = data.super_active || 0;
205
+ document.getElementById('stat-regular-keys').textContent = data.regular_active || 0;
206
+ } catch (error) {
207
+ console.error('Failed to load stats:', error);
208
+ }
209
+ }
210
+
211
+ async function loadKeys() {
212
+ try {
213
+ const response = await fetch(`${API_URL}/manage/keys`, {
214
+ headers: { 'Authorization': `Bearer ${superKey}` }
215
+ });
216
+
217
+ if (response.ok) {
218
+ const keys = await response.json();
219
+ renderKeys(keys);
220
+ }
221
+ } catch (error) {
222
+ console.error('Failed to load keys:', error);
223
+ }
224
+ }
225
+
226
+ async function loadRegularServerKeys() {
227
+ try {
228
+ const response = await fetch(`${API_URL}/manage/keys/server-keys`, {
229
+ headers: { 'Authorization': `Bearer ${superKey}` }
230
+ });
231
+
232
+ if (response.ok) {
233
+ const serverKeys = await response.json();
234
+ renderServerKeys(serverKeys);
235
+ } else {
236
+ document.getElementById('keys-list').innerHTML = '<p>无法加载Server Key列表</p>';
237
+ }
238
+ } catch (error) {
239
+ console.error('Failed to load server keys:', error);
240
+ document.getElementById('keys-list').innerHTML = '<p>无法加载Server Key列表</p>';
241
+ }
242
+ }
243
+
244
+ function renderServerKeys(keys) {
245
+ const keysList = document.getElementById('keys-list');
246
+
247
+ if (keys.length === 0) {
248
+ keysList.innerHTML = '<p>暂无关联的Server Key</p>';
249
+ return;
250
+ }
251
+
252
+ keysList.innerHTML = keys.map(key => `
253
+ <div class="key-card">
254
+ <div class="key-info">
255
+ <h3>
256
+ <span class="key-badge server">🖥️ Server</span>
257
+ <span class="key-badge ${key.isActive ? 'active' : 'inactive'}">
258
+ ${key.isActive ? '活跃' : '已停用'}
259
+ </span>
260
+ ${key.name}
261
+ </h3>
262
+ <p>ID: ${key.id}</p>
263
+ <p>前缀: ${key.keyPrefix}</p>
264
+ ${key.serverId ? `<p>服务器ID: ${key.serverId}</p>` : ''}
265
+ <p>创建时间: ${new Date(key.createdAt).toLocaleString('zh-CN')}</p>
266
+ <p>最后使用: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString('zh-CN') : '从未使用'}</p>
267
+ </div>
268
+ <div class="key-actions">
269
+ ${key.isActive ?
270
+ `<button class="btn-danger" onclick="deactivateKey('${key.id}')">停用</button>` :
271
+ `<button class="btn-success" onclick="activateKey('${key.id}')">激活</button>`
272
+ }
273
+ <button class="btn-danger" onclick="deleteKey('${key.id}', '${key.name}')">删除</button>
274
+ </div>
275
+ </div>
276
+ `).join('');
277
+ }
278
+
279
+ function renderKeys(keys) {
280
+ const keysList = document.getElementById('keys-list');
281
+
282
+ if (keys.length === 0) {
283
+ keysList.innerHTML = '<p>暂无API密钥</p>';
284
+ return;
285
+ }
286
+
287
+ keysList.innerHTML = keys.map(key => `
288
+ <div class="key-card ${key.keyType === 'admin' ? 'super' : ''}">
289
+ <div class="key-info">
290
+ <h3>
291
+ <span class="key-badge ${key.keyType}">
292
+ ${key.keyType === 'admin' ? '👑 Admin' : key.keyType === 'server' ? '🖥️ Server' : '🔑 Regular'}
293
+ </span>
294
+ <span class="key-badge ${key.isActive ? 'active' : 'inactive'}">
295
+ ${key.isActive ? '活跃' : '已停用'}
296
+ </span>
297
+ ${key.name}
298
+ </h3>
299
+ <p>ID: ${key.id}</p>
300
+ <p>前缀: ${key.keyPrefix}</p>
301
+ ${key.serverId ? `<p>服务器ID: ${key.serverId}</p>` : ''}
302
+ <p>创建时间: ${new Date(key.createdAt).toLocaleString('zh-CN')}</p>
303
+ <p>最后使用: ${key.lastUsed ? new Date(key.lastUsed).toLocaleString('zh-CN') : '从未使用'}</p>
304
+ </div>
305
+ <div class="key-actions">
306
+ ${key.isActive ?
307
+ `<button class="btn-danger" onclick="deactivateKey('${key.id}')">停用</button>` :
308
+ `<button class="btn-success" onclick="activateKey('${key.id}')">激活</button>`
309
+ }
310
+ <button class="btn-danger" onclick="deleteKey('${key.id}', '${key.name}')">删除</button>
311
+ </div>
312
+ </div>
313
+ `).join('');
314
+ }
315
+
316
+ async function activateKey(keyId) {
317
+ try {
318
+ const response = await fetch(`${API_URL}/manage/keys/${keyId}/activate`, {
319
+ method: 'PATCH',
320
+ headers: { 'Authorization': `Bearer ${superKey}` }
321
+ });
322
+
323
+ if (response.ok) {
324
+ if (currentUserKeyType === 'admin') {
325
+ loadKeys();
326
+ } else if (currentUserKeyType === 'regular') {
327
+ loadRegularServerKeys();
328
+ }
329
+ } else {
330
+ const error = await response.json();
331
+ alert('激活失败: ' + error.detail);
332
+ }
333
+ } catch (error) {
334
+ alert('激活失败: ' + error.message);
335
+ }
336
+ }
337
+
338
+ async function deactivateKey(keyId) {
339
+ try {
340
+ const response = await fetch(`${API_URL}/manage/keys/${keyId}/deactivate`, {
341
+ method: 'PATCH',
342
+ headers: { 'Authorization': `Bearer ${superKey}` }
343
+ });
344
+
345
+ if (response.ok) {
346
+ if (currentUserKeyType === 'admin') {
347
+ loadKeys();
348
+ } else if (currentUserKeyType === 'regular') {
349
+ loadRegularServerKeys();
350
+ }
351
+ } else {
352
+ const error = await response.json();
353
+ alert('停用失败: ' + error.detail);
354
+ }
355
+ } catch (error) {
356
+ alert('停用失败: ' + error.message);
357
+ }
358
+ }
359
+
360
+ async function deleteKey(keyId, keyName) {
361
+ if (!confirm(`确定要删除密钥 "${keyName}" 吗?此操作无法撤销。`)) {
362
+ return;
363
+ }
364
+
365
+ try {
366
+ const response = await fetch(`${API_URL}/manage/keys/${keyId}`, {
367
+ method: 'DELETE',
368
+ headers: { 'Authorization': `Bearer ${superKey}` }
369
+ });
370
+
371
+ if (response.ok) {
372
+ if (currentUserKeyType === 'admin') {
373
+ loadKeys();
374
+ } else if (currentUserKeyType === 'regular') {
375
+ loadRegularServerKeys();
376
+ }
377
+ } else {
378
+ const error = await response.json();
379
+ alert('删除失败: ' + error.detail);
380
+ }
381
+ } catch (error) {
382
+ alert('删除失败: ' + error.message);
383
+ }
384
+ }
385
+
386
+ function connectWebSocket() {
387
+ if (!superKey) return;
388
+
389
+ const wsUrl = `ws://localhost:8000/ws?api_key=${superKey}`;
390
+ ws = new WebSocket(wsUrl);
391
+
392
+ ws.onopen = () => {
393
+ console.log('WebSocket connected');
394
+ };
395
+
396
+ ws.onmessage = (event) => {
397
+ try {
398
+ const message = JSON.parse(event.data);
399
+
400
+ if (message.type === 'minecraft_event') {
401
+ addEventToList(message.event);
402
+ }
403
+ } catch (error) {
404
+ console.error('Failed to parse WebSocket message:', error);
405
+ }
406
+ };
407
+
408
+ ws.onerror = (error) => {
409
+ console.error('WebSocket error:', error);
410
+ };
411
+
412
+ ws.onclose = () => {
413
+ console.log('WebSocket disconnected');
414
+ setTimeout(() => {
415
+ if (superKey) {
416
+ connectWebSocket();
417
+ }
418
+ }, 5000);
419
+ };
420
+
421
+ setInterval(() => {
422
+ if (ws && ws.readyState === WebSocket.OPEN) {
423
+ ws.send(JSON.stringify({ type: 'ping' }));
424
+ }
425
+ }, 30000);
426
+ }
427
+
428
+ function addEventToList(event) {
429
+ const eventsList = document.getElementById('events-list');
430
+
431
+ const eventItem = document.createElement('div');
432
+ eventItem.className = 'event-item';
433
+ eventItem.innerHTML = `
434
+ <strong>${event.event_type}</strong> - ${event.server_name}<br>
435
+ <small>${new Date(event.timestamp).toLocaleString('zh-CN')}</small><br>
436
+ <pre>${JSON.stringify(event.data, null, 2)}</pre>
437
+ `;
438
+
439
+ eventsList.insertBefore(eventItem, eventsList.firstChild);
440
+
441
+ while (eventsList.children.length > 50) {
442
+ eventsList.removeChild(eventsList.lastChild);
443
+ }
444
+ }
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Minecraft WebSocket API - 控制面板</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+ <body>
10
+ <div id="login-screen" class="screen">
11
+ <div class="login-container">
12
+ <h1>🎮 Minecraft WebSocket API</h1>
13
+ <h2>控制面板登录</h2>
14
+ <form id="login-form">
15
+ <input type="password" id="super-key-input" placeholder="输入Admin Key或Server Key" required>
16
+ <button type="submit">登录</button>
17
+ </form>
18
+ <p class="error-message" id="login-error"></p>
19
+ <div class="login-info">
20
+ <p><strong>密钥类型说明:</strong></p>
21
+ <p>👑 <strong>Admin Key</strong> - 完全管理权限</p>
22
+ <p>🖥️ <strong>Server Key</strong> - 服务器管理权限</p>
23
+ </div>
24
+ </div>
25
+ </div>
26
+
27
+ <div id="dashboard-screen" class="screen hidden">
28
+ <nav class="navbar">
29
+ <h1>🎮 Minecraft WebSocket API</h1>
30
+ <button id="logout-btn">退出登录</button>
31
+ </nav>
32
+
33
+ <div class="container">
34
+ <div class="stats-grid">
35
+ <div class="stat-card">
36
+ <h3>活跃连接</h3>
37
+ <p class="stat-value" id="stat-connections">0</p>
38
+ </div>
39
+ <div class="stat-card">
40
+ <h3>总密钥数</h3>
41
+ <p class="stat-value" id="stat-total-keys">0</p>
42
+ </div>
43
+ <div class="stat-card">
44
+ <h3>活跃SuperKeys</h3>
45
+ <p class="stat-value" id="stat-super-keys">0</p>
46
+ </div>
47
+ <div class="stat-card">
48
+ <h3>活跃普通Keys</h3>
49
+ <p class="stat-value" id="stat-regular-keys">0</p>
50
+ </div>
51
+ </div>
52
+
53
+ <div class="section">
54
+ <div class="section-header">
55
+ <h2>API密钥管理</h2>
56
+ <button id="create-key-btn" class="btn-primary">创建新密钥</button>
57
+ </div>
58
+ <div id="keys-list" class="keys-list"></div>
59
+ </div>
60
+
61
+ <div class="section">
62
+ <h2>实时事件监控</h2>
63
+ <div id="events-list" class="events-list"></div>
64
+ </div>
65
+ </div>
66
+ </div>
67
+
68
+ <div id="create-key-modal" class="modal hidden">
69
+ <div class="modal-content">
70
+ <h2>创建新API密钥</h2>
71
+ <form id="create-key-form">
72
+ <label>名称 *</label>
73
+ <input type="text" id="key-name" required>
74
+
75
+ <label>描述</label>
76
+ <textarea id="key-description"></textarea>
77
+
78
+ <label>
79
+ <input type="checkbox" id="key-is-super">
80
+ 创建为SuperKey
81
+ </label>
82
+
83
+ <div class="modal-actions">
84
+ <button type="button" id="cancel-create-btn" class="btn-secondary">取消</button>
85
+ <button type="submit" class="btn-primary">创建</button>
86
+ </div>
87
+ </form>
88
+ </div>
89
+ </div>
90
+
91
+ <div id="key-details-modal" class="modal hidden">
92
+ <div class="modal-content">
93
+ <h2>密钥详情</h2>
94
+ <div id="key-details-content"></div>
95
+ <div class="modal-actions">
96
+ <button id="close-details-btn" class="btn-secondary">关闭</button>
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ <script src="app.js"></script>
102
+ </body>
103
+ </html>
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/style.css ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
9
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
10
+ min-height: 100vh;
11
+ color: #333;
12
+ }
13
+
14
+ .screen {
15
+ min-height: 100vh;
16
+ }
17
+
18
+ .hidden {
19
+ display: none !important;
20
+ }
21
+
22
+ .login-container {
23
+ display: flex;
24
+ flex-direction: column;
25
+ align-items: center;
26
+ justify-content: center;
27
+ min-height: 100vh;
28
+ padding: 20px;
29
+ }
30
+
31
+ .login-container h1 {
32
+ color: white;
33
+ font-size: 3em;
34
+ margin-bottom: 10px;
35
+ }
36
+
37
+ .login-container h2 {
38
+ color: white;
39
+ font-size: 1.5em;
40
+ margin-bottom: 30px;
41
+ }
42
+
43
+ #login-form {
44
+ background: white;
45
+ padding: 40px;
46
+ border-radius: 10px;
47
+ box-shadow: 0 10px 40px rgba(0,0,0,0.2);
48
+ width: 100%;
49
+ max-width: 400px;
50
+ }
51
+
52
+ #login-form input {
53
+ width: 100%;
54
+ padding: 15px;
55
+ border: 2px solid #e0e0e0;
56
+ border-radius: 5px;
57
+ font-size: 16px;
58
+ margin-bottom: 20px;
59
+ }
60
+
61
+ #login-form button {
62
+ width: 100%;
63
+ padding: 15px;
64
+ background: #667eea;
65
+ color: white;
66
+ border: none;
67
+ border-radius: 5px;
68
+ font-size: 16px;
69
+ font-weight: bold;
70
+ cursor: pointer;
71
+ transition: background 0.3s;
72
+ }
73
+
74
+ #login-form button:hover {
75
+ background: #5568d3;
76
+ }
77
+
78
+ .error-message {
79
+ color: #ff4444;
80
+ margin-top: 10px;
81
+ text-align: center;
82
+ }
83
+
84
+ .navbar {
85
+ background: white;
86
+ padding: 20px 40px;
87
+ display: flex;
88
+ justify-content: space-between;
89
+ align-items: center;
90
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
91
+ }
92
+
93
+ .navbar h1 {
94
+ font-size: 1.5em;
95
+ color: #667eea;
96
+ }
97
+
98
+ #logout-btn {
99
+ padding: 10px 20px;
100
+ background: #ff4444;
101
+ color: white;
102
+ border: none;
103
+ border-radius: 5px;
104
+ cursor: pointer;
105
+ font-weight: bold;
106
+ }
107
+
108
+ .container {
109
+ max-width: 1400px;
110
+ margin: 0 auto;
111
+ padding: 40px 20px;
112
+ }
113
+
114
+ .stats-grid {
115
+ display: grid;
116
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
117
+ gap: 20px;
118
+ margin-bottom: 40px;
119
+ }
120
+
121
+ .stat-card {
122
+ background: white;
123
+ padding: 30px;
124
+ border-radius: 10px;
125
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
126
+ text-align: center;
127
+ }
128
+
129
+ .stat-card h3 {
130
+ color: #666;
131
+ font-size: 0.9em;
132
+ margin-bottom: 10px;
133
+ text-transform: uppercase;
134
+ }
135
+
136
+ .stat-value {
137
+ font-size: 3em;
138
+ font-weight: bold;
139
+ color: #667eea;
140
+ }
141
+
142
+ .section {
143
+ background: white;
144
+ padding: 30px;
145
+ border-radius: 10px;
146
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
147
+ margin-bottom: 30px;
148
+ }
149
+
150
+ .section-header {
151
+ display: flex;
152
+ justify-content: space-between;
153
+ align-items: center;
154
+ margin-bottom: 20px;
155
+ }
156
+
157
+ .section h2 {
158
+ color: #333;
159
+ margin-bottom: 20px;
160
+ }
161
+
162
+ .btn-primary {
163
+ padding: 12px 24px;
164
+ background: #667eea;
165
+ color: white;
166
+ border: none;
167
+ border-radius: 5px;
168
+ cursor: pointer;
169
+ font-weight: bold;
170
+ transition: background 0.3s;
171
+ }
172
+
173
+ .btn-primary:hover {
174
+ background: #5568d3;
175
+ }
176
+
177
+ .btn-secondary {
178
+ padding: 12px 24px;
179
+ background: #e0e0e0;
180
+ color: #333;
181
+ border: none;
182
+ border-radius: 5px;
183
+ cursor: pointer;
184
+ font-weight: bold;
185
+ }
186
+
187
+ .btn-danger {
188
+ padding: 8px 16px;
189
+ background: #ff4444;
190
+ color: white;
191
+ border: none;
192
+ border-radius: 5px;
193
+ cursor: pointer;
194
+ font-size: 0.9em;
195
+ }
196
+
197
+ .btn-success {
198
+ padding: 8px 16px;
199
+ background: #4caf50;
200
+ color: white;
201
+ border: none;
202
+ border-radius: 5px;
203
+ cursor: pointer;
204
+ font-size: 0.9em;
205
+ }
206
+
207
+ .keys-list {
208
+ display: grid;
209
+ gap: 15px;
210
+ }
211
+
212
+ .key-card {
213
+ border: 2px solid #e0e0e0;
214
+ padding: 20px;
215
+ border-radius: 8px;
216
+ display: flex;
217
+ justify-content: space-between;
218
+ align-items: center;
219
+ }
220
+
221
+ .key-card.super {
222
+ border-color: #764ba2;
223
+ background: #f9f7fb;
224
+ }
225
+
226
+ .key-info h3 {
227
+ margin-bottom: 5px;
228
+ }
229
+
230
+ .key-badge {
231
+ display: inline-block;
232
+ padding: 4px 8px;
233
+ border-radius: 4px;
234
+ font-size: 0.8em;
235
+ font-weight: bold;
236
+ margin-right: 10px;
237
+ }
238
+
239
+ .key-badge.super {
240
+ background: #764ba2;
241
+ color: white;
242
+ }
243
+
244
+ .key-badge.regular {
245
+ background: #667eea;
246
+ color: white;
247
+ }
248
+
249
+ .key-badge.active {
250
+ background: #4caf50;
251
+ color: white;
252
+ }
253
+
254
+ .key-badge.inactive {
255
+ background: #ff4444;
256
+ color: white;
257
+ }
258
+
259
+ .key-actions {
260
+ display: flex;
261
+ gap: 10px;
262
+ }
263
+
264
+ .events-list {
265
+ max-height: 400px;
266
+ overflow-y: auto;
267
+ }
268
+
269
+ .event-item {
270
+ padding: 15px;
271
+ border-left: 4px solid #667eea;
272
+ background: #f5f5f5;
273
+ margin-bottom: 10px;
274
+ border-radius: 4px;
275
+ }
276
+
277
+ .event-item strong {
278
+ color: #667eea;
279
+ }
280
+
281
+ .modal {
282
+ position: fixed;
283
+ top: 0;
284
+ left: 0;
285
+ width: 100%;
286
+ height: 100%;
287
+ background: rgba(0,0,0,0.5);
288
+ display: flex;
289
+ justify-content: center;
290
+ align-items: center;
291
+ z-index: 1000;
292
+ }
293
+
294
+ .modal-content {
295
+ background: white;
296
+ padding: 40px;
297
+ border-radius: 10px;
298
+ max-width: 500px;
299
+ width: 90%;
300
+ max-height: 90vh;
301
+ overflow-y: auto;
302
+ }
303
+
304
+ .modal-content h2 {
305
+ margin-bottom: 20px;
306
+ }
307
+
308
+ .modal-content label {
309
+ display: block;
310
+ margin-bottom: 5px;
311
+ font-weight: bold;
312
+ color: #666;
313
+ }
314
+
315
+ .modal-content input[type="text"],
316
+ .modal-content textarea {
317
+ width: 100%;
318
+ padding: 10px;
319
+ border: 2px solid #e0e0e0;
320
+ border-radius: 5px;
321
+ margin-bottom: 15px;
322
+ font-size: 14px;
323
+ }
324
+
325
+ .modal-content textarea {
326
+ min-height: 80px;
327
+ resize: vertical;
328
+ }
329
+
330
+ .modal-content input[type="checkbox"] {
331
+ margin-right: 8px;
332
+ }
333
+
334
+ .modal-actions {
335
+ display: flex;
336
+ gap: 10px;
337
+ justify-content: flex-end;
338
+ margin-top: 20px;
339
+ }
340
+
341
+ .nav-info {
342
+ display: flex;
343
+ align-items: center;
344
+ gap: 15px;
345
+ }
346
+
347
+ #user-info {
348
+ font-size: 0.9em;
349
+ color: #667eea;
350
+ background: rgba(255,255,255,0.1);
351
+ padding: 8px 12px;
352
+ border-radius: 20px;
353
+ }
354
+
355
+ .server-info {
356
+ display: grid;
357
+ grid-template-columns: 1fr 1fr;
358
+ gap: 20px;
359
+ }
360
+
361
+ .info-card {
362
+ background: #f8f9fa;
363
+ padding: 20px;
364
+ border-radius: 8px;
365
+ border: 1px solid #e9ecef;
366
+ }
367
+
368
+ .info-card h3 {
369
+ margin-bottom: 15px;
370
+ color: #495057;
371
+ }
372
+
373
+ .command-history {
374
+ max-height: 200px;
375
+ overflow-y: auto;
376
+ background: white;
377
+ border: 1px solid #e9ecef;
378
+ border-radius: 4px;
379
+ padding: 10px;
380
+ }
381
+
382
+ .command-item {
383
+ padding: 8px 0;
384
+ border-bottom: 1px solid #f8f9fa;
385
+ font-family: monospace;
386
+ font-size: 0.9em;
387
+ }
388
+
389
+ .command-item:last-child {
390
+ border-bottom: none;
391
+ }
392
+
393
+ .command-item .timestamp {
394
+ color: #6c757d;
395
+ font-size: 0.8em;
396
+ }
397
+
398
+ .login-info {
399
+ margin-top: 20px;
400
+ text-align: left;
401
+ background: rgba(255,255,255,0.1);
402
+ padding: 15px;
403
+ border-radius: 8px;
404
+ }
405
+
406
+ .login-info p {
407
+ margin: 5px 0;
408
+ font-size: 0.9em;
409
+ color: white;
410
+ }
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/server.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ require('dotenv').config();
2
+ const express = require('express');
3
+ const path = require('path');
4
+
5
+ const app = express();
6
+ const PORT = parseInt(process.env.DASHBOARD_PORT || '3000');
7
+
8
+ app.use(express.static(path.join(__dirname, 'public')));
9
+
10
+ app.get('/', (req, res) => {
11
+ res.sendFile(path.join(__dirname, 'public', 'index.html'));
12
+ });
13
+
14
+ app.listen(PORT, () => {
15
+ console.log('='.repeat(50));
16
+ console.log('🎮 Minecraft WebSocket API - 控制面板');
17
+ console.log('='.repeat(50));
18
+ console.log(`控制面板地址: http://localhost:${PORT}`);
19
+ console.log('请使用SuperKey登录');
20
+ console.log('='.repeat(50));
21
+ });
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docker-compose.yml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ interconnect-server:
5
+ build: .
6
+ ports:
7
+ - "8000:8000"
8
+ environment:
9
+ - SERVER_HOST=0.0.0.0
10
+ - SERVER_PORT=8000
11
+ - DATABASE_PATH=/data/minecraft_ws.db
12
+ volumes:
13
+ - ./data:/data
14
+ restart: unless-stopped
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/package-lock.json ADDED
@@ -0,0 +1,675 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "interconnect-server-node",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 1,
5
+ "requires": true,
6
+ "dependencies": {
7
+ "accepts": {
8
+ "version": "1.3.8",
9
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
10
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
11
+ "requires": {
12
+ "mime-types": "~2.1.34",
13
+ "negotiator": "0.6.3"
14
+ }
15
+ },
16
+ "array-flatten": {
17
+ "version": "1.1.1",
18
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
19
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
20
+ },
21
+ "asynckit": {
22
+ "version": "0.4.0",
23
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
24
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
25
+ },
26
+ "axios": {
27
+ "version": "1.13.2",
28
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
29
+ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
30
+ "requires": {
31
+ "follow-redirects": "^1.15.6",
32
+ "form-data": "^4.0.4",
33
+ "proxy-from-env": "^1.1.0"
34
+ }
35
+ },
36
+ "bcryptjs": {
37
+ "version": "2.4.3",
38
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
39
+ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
40
+ },
41
+ "body-parser": {
42
+ "version": "1.20.4",
43
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
44
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
45
+ "requires": {
46
+ "bytes": "~3.1.2",
47
+ "content-type": "~1.0.5",
48
+ "debug": "2.6.9",
49
+ "depd": "2.0.0",
50
+ "destroy": "~1.2.0",
51
+ "http-errors": "~2.0.1",
52
+ "iconv-lite": "~0.4.24",
53
+ "on-finished": "~2.4.1",
54
+ "qs": "~6.14.0",
55
+ "raw-body": "~2.5.3",
56
+ "type-is": "~1.6.18",
57
+ "unpipe": "~1.0.0"
58
+ },
59
+ "dependencies": {
60
+ "debug": {
61
+ "version": "2.6.9",
62
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
63
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
64
+ "requires": {
65
+ "ms": "2.0.0"
66
+ }
67
+ },
68
+ "ms": {
69
+ "version": "2.0.0",
70
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
71
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
72
+ }
73
+ }
74
+ },
75
+ "bytes": {
76
+ "version": "3.1.2",
77
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
78
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
79
+ },
80
+ "call-bind-apply-helpers": {
81
+ "version": "1.0.2",
82
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
83
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
84
+ "requires": {
85
+ "es-errors": "^1.3.0",
86
+ "function-bind": "^1.1.2"
87
+ }
88
+ },
89
+ "call-bound": {
90
+ "version": "1.0.4",
91
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
92
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
93
+ "requires": {
94
+ "call-bind-apply-helpers": "^1.0.2",
95
+ "get-intrinsic": "^1.3.0"
96
+ }
97
+ },
98
+ "combined-stream": {
99
+ "version": "1.0.8",
100
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
101
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
102
+ "requires": {
103
+ "delayed-stream": "~1.0.0"
104
+ }
105
+ },
106
+ "commander": {
107
+ "version": "11.1.0",
108
+ "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
109
+ "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="
110
+ },
111
+ "content-disposition": {
112
+ "version": "0.5.4",
113
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
114
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
115
+ "requires": {
116
+ "safe-buffer": "5.2.1"
117
+ }
118
+ },
119
+ "content-type": {
120
+ "version": "1.0.5",
121
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
122
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
123
+ },
124
+ "cookie": {
125
+ "version": "0.7.2",
126
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
127
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="
128
+ },
129
+ "cookie-signature": {
130
+ "version": "1.0.7",
131
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
132
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
133
+ },
134
+ "delayed-stream": {
135
+ "version": "1.0.0",
136
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
137
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
138
+ },
139
+ "depd": {
140
+ "version": "2.0.0",
141
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
142
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
143
+ },
144
+ "destroy": {
145
+ "version": "1.2.0",
146
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
147
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
148
+ },
149
+ "dotenv": {
150
+ "version": "16.6.1",
151
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
152
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="
153
+ },
154
+ "dunder-proto": {
155
+ "version": "1.0.1",
156
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
157
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
158
+ "requires": {
159
+ "call-bind-apply-helpers": "^1.0.1",
160
+ "es-errors": "^1.3.0",
161
+ "gopd": "^1.2.0"
162
+ }
163
+ },
164
+ "ee-first": {
165
+ "version": "1.1.1",
166
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
167
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
168
+ },
169
+ "encodeurl": {
170
+ "version": "2.0.0",
171
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
172
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
173
+ },
174
+ "es-define-property": {
175
+ "version": "1.0.1",
176
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
177
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
178
+ },
179
+ "es-errors": {
180
+ "version": "1.3.0",
181
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
182
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
183
+ },
184
+ "es-object-atoms": {
185
+ "version": "1.1.1",
186
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
187
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
188
+ "requires": {
189
+ "es-errors": "^1.3.0"
190
+ }
191
+ },
192
+ "es-set-tostringtag": {
193
+ "version": "2.1.0",
194
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
195
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
196
+ "requires": {
197
+ "es-errors": "^1.3.0",
198
+ "get-intrinsic": "^1.2.6",
199
+ "has-tostringtag": "^1.0.2",
200
+ "hasown": "^2.0.2"
201
+ }
202
+ },
203
+ "escape-html": {
204
+ "version": "1.0.3",
205
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
206
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
207
+ },
208
+ "etag": {
209
+ "version": "1.8.1",
210
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
211
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
212
+ },
213
+ "express": {
214
+ "version": "4.22.1",
215
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
216
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
217
+ "requires": {
218
+ "accepts": "~1.3.8",
219
+ "array-flatten": "1.1.1",
220
+ "body-parser": "~1.20.3",
221
+ "content-disposition": "~0.5.4",
222
+ "content-type": "~1.0.4",
223
+ "cookie": "~0.7.1",
224
+ "cookie-signature": "~1.0.6",
225
+ "debug": "2.6.9",
226
+ "depd": "2.0.0",
227
+ "encodeurl": "~2.0.0",
228
+ "escape-html": "~1.0.3",
229
+ "etag": "~1.8.1",
230
+ "finalhandler": "~1.3.1",
231
+ "fresh": "~0.5.2",
232
+ "http-errors": "~2.0.0",
233
+ "merge-descriptors": "1.0.3",
234
+ "methods": "~1.1.2",
235
+ "on-finished": "~2.4.1",
236
+ "parseurl": "~1.3.3",
237
+ "path-to-regexp": "~0.1.12",
238
+ "proxy-addr": "~2.0.7",
239
+ "qs": "~6.14.0",
240
+ "range-parser": "~1.2.1",
241
+ "safe-buffer": "5.2.1",
242
+ "send": "~0.19.0",
243
+ "serve-static": "~1.16.2",
244
+ "setprototypeof": "1.2.0",
245
+ "statuses": "~2.0.1",
246
+ "type-is": "~1.6.18",
247
+ "utils-merge": "1.0.1",
248
+ "vary": "~1.1.2"
249
+ },
250
+ "dependencies": {
251
+ "debug": {
252
+ "version": "2.6.9",
253
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
254
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
255
+ "requires": {
256
+ "ms": "2.0.0"
257
+ }
258
+ },
259
+ "ms": {
260
+ "version": "2.0.0",
261
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
262
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
263
+ }
264
+ }
265
+ },
266
+ "finalhandler": {
267
+ "version": "1.3.2",
268
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
269
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
270
+ "requires": {
271
+ "debug": "2.6.9",
272
+ "encodeurl": "~2.0.0",
273
+ "escape-html": "~1.0.3",
274
+ "on-finished": "~2.4.1",
275
+ "parseurl": "~1.3.3",
276
+ "statuses": "~2.0.2",
277
+ "unpipe": "~1.0.0"
278
+ },
279
+ "dependencies": {
280
+ "debug": {
281
+ "version": "2.6.9",
282
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
283
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
284
+ "requires": {
285
+ "ms": "2.0.0"
286
+ }
287
+ },
288
+ "ms": {
289
+ "version": "2.0.0",
290
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
291
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
292
+ }
293
+ }
294
+ },
295
+ "follow-redirects": {
296
+ "version": "1.15.11",
297
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
298
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="
299
+ },
300
+ "form-data": {
301
+ "version": "4.0.5",
302
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
303
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
304
+ "requires": {
305
+ "asynckit": "^0.4.0",
306
+ "combined-stream": "^1.0.8",
307
+ "es-set-tostringtag": "^2.1.0",
308
+ "hasown": "^2.0.2",
309
+ "mime-types": "^2.1.12"
310
+ }
311
+ },
312
+ "forwarded": {
313
+ "version": "0.2.0",
314
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
315
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
316
+ },
317
+ "fresh": {
318
+ "version": "0.5.2",
319
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
320
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
321
+ },
322
+ "function-bind": {
323
+ "version": "1.1.2",
324
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
325
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
326
+ },
327
+ "get-intrinsic": {
328
+ "version": "1.3.0",
329
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
330
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
331
+ "requires": {
332
+ "call-bind-apply-helpers": "^1.0.2",
333
+ "es-define-property": "^1.0.1",
334
+ "es-errors": "^1.3.0",
335
+ "es-object-atoms": "^1.1.1",
336
+ "function-bind": "^1.1.2",
337
+ "get-proto": "^1.0.1",
338
+ "gopd": "^1.2.0",
339
+ "has-symbols": "^1.1.0",
340
+ "hasown": "^2.0.2",
341
+ "math-intrinsics": "^1.1.0"
342
+ }
343
+ },
344
+ "get-proto": {
345
+ "version": "1.0.1",
346
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
347
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
348
+ "requires": {
349
+ "dunder-proto": "^1.0.1",
350
+ "es-object-atoms": "^1.0.0"
351
+ }
352
+ },
353
+ "gopd": {
354
+ "version": "1.2.0",
355
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
356
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
357
+ },
358
+ "has-symbols": {
359
+ "version": "1.1.0",
360
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
361
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
362
+ },
363
+ "has-tostringtag": {
364
+ "version": "1.0.2",
365
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
366
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
367
+ "requires": {
368
+ "has-symbols": "^1.0.3"
369
+ }
370
+ },
371
+ "hasown": {
372
+ "version": "2.0.2",
373
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
374
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
375
+ "requires": {
376
+ "function-bind": "^1.1.2"
377
+ }
378
+ },
379
+ "http-errors": {
380
+ "version": "2.0.1",
381
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
382
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
383
+ "requires": {
384
+ "depd": "~2.0.0",
385
+ "inherits": "~2.0.4",
386
+ "setprototypeof": "~1.2.0",
387
+ "statuses": "~2.0.2",
388
+ "toidentifier": "~1.0.1"
389
+ }
390
+ },
391
+ "iconv-lite": {
392
+ "version": "0.4.24",
393
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
394
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
395
+ "requires": {
396
+ "safer-buffer": ">= 2.1.2 < 3"
397
+ }
398
+ },
399
+ "inherits": {
400
+ "version": "2.0.4",
401
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
402
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
403
+ },
404
+ "ipaddr.js": {
405
+ "version": "1.9.1",
406
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
407
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
408
+ },
409
+ "math-intrinsics": {
410
+ "version": "1.1.0",
411
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
412
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
413
+ },
414
+ "media-typer": {
415
+ "version": "0.3.0",
416
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
417
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
418
+ },
419
+ "merge-descriptors": {
420
+ "version": "1.0.3",
421
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
422
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="
423
+ },
424
+ "methods": {
425
+ "version": "1.1.2",
426
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
427
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
428
+ },
429
+ "mime": {
430
+ "version": "1.6.0",
431
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
432
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
433
+ },
434
+ "mime-db": {
435
+ "version": "1.52.0",
436
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
437
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
438
+ },
439
+ "mime-types": {
440
+ "version": "2.1.35",
441
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
442
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
443
+ "requires": {
444
+ "mime-db": "1.52.0"
445
+ }
446
+ },
447
+ "ms": {
448
+ "version": "2.1.3",
449
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
450
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
451
+ },
452
+ "negotiator": {
453
+ "version": "0.6.3",
454
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
455
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
456
+ },
457
+ "object-inspect": {
458
+ "version": "1.13.4",
459
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
460
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
461
+ },
462
+ "on-finished": {
463
+ "version": "2.4.1",
464
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
465
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
466
+ "requires": {
467
+ "ee-first": "1.1.1"
468
+ }
469
+ },
470
+ "parseurl": {
471
+ "version": "1.3.3",
472
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
473
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
474
+ },
475
+ "path-to-regexp": {
476
+ "version": "0.1.12",
477
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
478
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
479
+ },
480
+ "proxy-addr": {
481
+ "version": "2.0.7",
482
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
483
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
484
+ "requires": {
485
+ "forwarded": "0.2.0",
486
+ "ipaddr.js": "1.9.1"
487
+ }
488
+ },
489
+ "proxy-from-env": {
490
+ "version": "1.1.0",
491
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
492
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
493
+ },
494
+ "qs": {
495
+ "version": "6.14.1",
496
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
497
+ "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
498
+ "requires": {
499
+ "side-channel": "^1.1.0"
500
+ }
501
+ },
502
+ "range-parser": {
503
+ "version": "1.2.1",
504
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
505
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
506
+ },
507
+ "raw-body": {
508
+ "version": "2.5.3",
509
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
510
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
511
+ "requires": {
512
+ "bytes": "~3.1.2",
513
+ "http-errors": "~2.0.1",
514
+ "iconv-lite": "~0.4.24",
515
+ "unpipe": "~1.0.0"
516
+ }
517
+ },
518
+ "safe-buffer": {
519
+ "version": "5.2.1",
520
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
521
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
522
+ },
523
+ "safer-buffer": {
524
+ "version": "2.1.2",
525
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
526
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
527
+ },
528
+ "send": {
529
+ "version": "0.19.2",
530
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
531
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
532
+ "requires": {
533
+ "debug": "2.6.9",
534
+ "depd": "2.0.0",
535
+ "destroy": "1.2.0",
536
+ "encodeurl": "~2.0.0",
537
+ "escape-html": "~1.0.3",
538
+ "etag": "~1.8.1",
539
+ "fresh": "~0.5.2",
540
+ "http-errors": "~2.0.1",
541
+ "mime": "1.6.0",
542
+ "ms": "2.1.3",
543
+ "on-finished": "~2.4.1",
544
+ "range-parser": "~1.2.1",
545
+ "statuses": "~2.0.2"
546
+ },
547
+ "dependencies": {
548
+ "debug": {
549
+ "version": "2.6.9",
550
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
551
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
552
+ "requires": {
553
+ "ms": "2.0.0"
554
+ },
555
+ "dependencies": {
556
+ "ms": {
557
+ "version": "2.0.0",
558
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
559
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
560
+ }
561
+ }
562
+ }
563
+ }
564
+ },
565
+ "serve-static": {
566
+ "version": "1.16.3",
567
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
568
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
569
+ "requires": {
570
+ "encodeurl": "~2.0.0",
571
+ "escape-html": "~1.0.3",
572
+ "parseurl": "~1.3.3",
573
+ "send": "~0.19.1"
574
+ }
575
+ },
576
+ "setprototypeof": {
577
+ "version": "1.2.0",
578
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
579
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
580
+ },
581
+ "side-channel": {
582
+ "version": "1.1.0",
583
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
584
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
585
+ "requires": {
586
+ "es-errors": "^1.3.0",
587
+ "object-inspect": "^1.13.3",
588
+ "side-channel-list": "^1.0.0",
589
+ "side-channel-map": "^1.0.1",
590
+ "side-channel-weakmap": "^1.0.2"
591
+ }
592
+ },
593
+ "side-channel-list": {
594
+ "version": "1.0.0",
595
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
596
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
597
+ "requires": {
598
+ "es-errors": "^1.3.0",
599
+ "object-inspect": "^1.13.3"
600
+ }
601
+ },
602
+ "side-channel-map": {
603
+ "version": "1.0.1",
604
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
605
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
606
+ "requires": {
607
+ "call-bound": "^1.0.2",
608
+ "es-errors": "^1.3.0",
609
+ "get-intrinsic": "^1.2.5",
610
+ "object-inspect": "^1.13.3"
611
+ }
612
+ },
613
+ "side-channel-weakmap": {
614
+ "version": "1.0.2",
615
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
616
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
617
+ "requires": {
618
+ "call-bound": "^1.0.2",
619
+ "es-errors": "^1.3.0",
620
+ "get-intrinsic": "^1.2.5",
621
+ "object-inspect": "^1.13.3",
622
+ "side-channel-map": "^1.0.1"
623
+ }
624
+ },
625
+ "sql.js": {
626
+ "version": "1.13.0",
627
+ "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.13.0.tgz",
628
+ "integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA=="
629
+ },
630
+ "statuses": {
631
+ "version": "2.0.2",
632
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
633
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="
634
+ },
635
+ "toidentifier": {
636
+ "version": "1.0.1",
637
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
638
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
639
+ },
640
+ "type-is": {
641
+ "version": "1.6.18",
642
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
643
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
644
+ "requires": {
645
+ "media-typer": "0.3.0",
646
+ "mime-types": "~2.1.24"
647
+ }
648
+ },
649
+ "unpipe": {
650
+ "version": "1.0.0",
651
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
652
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
653
+ },
654
+ "utils-merge": {
655
+ "version": "1.0.1",
656
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
657
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
658
+ },
659
+ "uuid": {
660
+ "version": "9.0.1",
661
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
662
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="
663
+ },
664
+ "vary": {
665
+ "version": "1.1.2",
666
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
667
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
668
+ },
669
+ "ws": {
670
+ "version": "8.19.0",
671
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
672
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="
673
+ }
674
+ }
675
+ }
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/package.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "interconnect-server",
3
+ "version": "1.0.0",
4
+ "description": "InterConnect-Server - Minecraft WebSocket API Server",
5
+ "author": "",
6
+ "license": "MIT",
7
+ "main": "src/server.js",
8
+ "scripts": {
9
+ "start": "node src/server.js",
10
+ "dev": "node --watch src/server.js",
11
+ "cli": "node cli/cli.js",
12
+ "dashboard": "node dashboard/server.js"
13
+ },
14
+ "keywords": [
15
+ "minecraft",
16
+ "websocket",
17
+ "api"
18
+ ],
19
+ "author": "",
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "axios": "^1.13.2",
23
+ "bcryptjs": "^2.4.3",
24
+ "commander": "^11.1.0",
25
+ "dotenv": "^16.3.1",
26
+ "express": "^4.18.2",
27
+ "sql.js": "^1.10.3",
28
+ "uuid": "^9.0.1",
29
+ "ws": "^8.14.2"
30
+ }
31
+ }
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/auth.js ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { KEY_TYPE_ADMIN, KEY_TYPE_SERVER, KEY_TYPE_REGULAR } = require('./database');
2
+
3
+ function verifyApiKey(db) {
4
+ return async (req, res, next) => {
5
+ const authHeader = req.headers.authorization;
6
+
7
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
8
+ return res.status(401).json({ detail: 'Invalid API Key' });
9
+ }
10
+
11
+ const token = authHeader.substring(7);
12
+ const result = await db.verifyApiKey(token);
13
+
14
+ if (!result) {
15
+ return res.status(401).json({ detail: 'Invalid API Key' });
16
+ }
17
+
18
+ req.apiKey = result;
19
+ next();
20
+ };
21
+ }
22
+
23
+ function requireAdminKey(req, res, next) {
24
+ if (!req.apiKey || req.apiKey.keyType !== KEY_TYPE_ADMIN) {
25
+ return res.status(403).json({ detail: 'Operation requires an Admin Key' });
26
+ }
27
+ next();
28
+ }
29
+
30
+ function requireRegularOrAdminKey(req, res, next) {
31
+ if (!req.apiKey || (req.apiKey.keyType !== KEY_TYPE_ADMIN && req.apiKey.keyType !== KEY_TYPE_REGULAR)) {
32
+ return res.status(403).json({ detail: 'Operation requires a Regular Key or Admin Key' });
33
+ }
34
+ next();
35
+ }
36
+
37
+ function requireRegularKeyForOwnServerKeys(db) {
38
+ return async (req, res, next) => {
39
+ requireRegularOrAdminKey(req, res, async () => {
40
+ const keyId = req.params.key_id;
41
+ const keyInfo = db.getApiKeyDetailsById(keyId);
42
+
43
+ if (!keyInfo) {
44
+ return res.status(404).json({ detail: 'Key not found' });
45
+ }
46
+
47
+ // Admin可以管理所有密钥
48
+ if (req.apiKey.keyType === KEY_TYPE_ADMIN) {
49
+ return next();
50
+ }
51
+
52
+ // Regular Key只能管理自己关联的Server Key
53
+ if (req.apiKey.keyType === KEY_TYPE_REGULAR && keyInfo.regularKeyId === req.apiKey.id) {
54
+ return next();
55
+ }
56
+
57
+ return res.status(403).json({ detail: 'Operation not permitted for this key' });
58
+ });
59
+ };
60
+ }
61
+
62
+ function requireServerKey(req, res, next) {
63
+ if (!req.apiKey || (req.apiKey.keyType !== KEY_TYPE_ADMIN && req.apiKey.keyType !== KEY_TYPE_SERVER)) {
64
+ return res.status(403).json({ detail: 'Operation requires a Server Key or Admin Key' });
65
+ }
66
+ next();
67
+ }
68
+
69
+ function requireAnyKey(req, res, next) {
70
+ if (!req.apiKey) {
71
+ return res.status(401).json({ detail: 'Authentication required' });
72
+ }
73
+ next();
74
+ }
75
+
76
+ module.exports = {
77
+ verifyApiKey,
78
+ requireAdminKey,
79
+ requireServerKey,
80
+ requireRegularOrAdminKey,
81
+ requireRegularKeyForOwnServerKeys,
82
+ requireAnyKey
83
+ };
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/database.js ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const initSqlJs = require('sql.js');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const bcrypt = require('bcryptjs');
5
+ const { v4: uuidv4 } = require('uuid');
6
+
7
+ const KEY_PREFIX_ADMIN = 'mc_admin_';
8
+ const KEY_PREFIX_SERVER = 'mc_server_';
9
+ const KEY_PREFIX_REGULAR = 'mc_key_';
10
+
11
+ const KEY_TYPE_ADMIN = 'admin';
12
+ const KEY_TYPE_SERVER = 'server';
13
+ const KEY_TYPE_REGULAR = 'regular';
14
+
15
+ class Database {
16
+ constructor(dbPath = 'minecraft_ws.db') {
17
+ this.dbPath = dbPath;
18
+ this.db = null;
19
+ }
20
+
21
+ async init() {
22
+ const SQL = await initSqlJs();
23
+
24
+ if (fs.existsSync(this.dbPath)) {
25
+ const buffer = fs.readFileSync(this.dbPath);
26
+ this.db = new SQL.Database(buffer);
27
+ } else {
28
+ this.db = new SQL.Database();
29
+ }
30
+
31
+ this.initDbStructure();
32
+ this.save();
33
+ }
34
+
35
+ initDbStructure() {
36
+ this.db.run(`
37
+ CREATE TABLE IF NOT EXISTS api_keys (
38
+ id TEXT PRIMARY KEY,
39
+ name TEXT NOT NULL,
40
+ description TEXT,
41
+ key_hash TEXT NOT NULL UNIQUE,
42
+ key_prefix TEXT NOT NULL,
43
+ key_type TEXT NOT NULL,
44
+ server_id TEXT,
45
+ regular_key_id TEXT,
46
+ created_at TEXT NOT NULL,
47
+ last_used TEXT,
48
+ is_active INTEGER DEFAULT 1
49
+ )
50
+ `);
51
+
52
+ this.db.run(`
53
+ CREATE TABLE IF NOT EXISTS event_logs (
54
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55
+ event_type TEXT NOT NULL,
56
+ server_name TEXT NOT NULL,
57
+ timestamp TEXT NOT NULL,
58
+ data TEXT NOT NULL,
59
+ api_key_id TEXT,
60
+ FOREIGN KEY (api_key_id) REFERENCES api_keys (id)
61
+ )
62
+ `);
63
+ }
64
+
65
+ save() {
66
+ const data = this.db.export();
67
+ const buffer = Buffer.from(data);
68
+ fs.writeFileSync(this.dbPath, buffer);
69
+ }
70
+
71
+ generateRandomRawKeyString(prefix) {
72
+ return `${prefix}${uuidv4().replace(/-/g, '')}`;
73
+ }
74
+
75
+ async ensureInitialAdminKey() {
76
+ const stmt = this.db.prepare('SELECT 1 FROM api_keys WHERE key_type = ? LIMIT 1');
77
+ stmt.bind([KEY_TYPE_ADMIN]);
78
+ const adminKeyExists = stmt.step();
79
+ stmt.free();
80
+
81
+ if (!adminKeyExists) {
82
+ const generatedRawAdminKey = this.generateRandomRawKeyString(KEY_PREFIX_ADMIN);
83
+ const adminKeyName = 'Auto-Generated Admin Key';
84
+ const adminKeyDescription = `Auto-generated on ${new Date().toISOString()}`;
85
+
86
+ const keyId = uuidv4();
87
+ const keyHash = await bcrypt.hash(generatedRawAdminKey, 10);
88
+ const createdAt = new Date().toISOString();
89
+
90
+ try {
91
+ this.db.run(
92
+ 'INSERT INTO api_keys (id, name, description, key_hash, key_prefix, key_type, created_at, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, 1)',
93
+ [keyId, adminKeyName, adminKeyDescription, keyHash, KEY_PREFIX_ADMIN, KEY_TYPE_ADMIN, createdAt]
94
+ );
95
+ this.save();
96
+ return { name: adminKeyName, key: generatedRawAdminKey };
97
+ } catch (error) {
98
+ console.error('Error creating initial Admin Key:', error);
99
+ return null;
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+
105
+ async createRegularKeyWithServerKey(name, description = '', serverId = null) {
106
+ const regularKeyId = uuidv4();
107
+ const serverKeyId = uuidv4();
108
+
109
+ // 创建Regular Key
110
+ const regularRawKey = this.generateRandomRawKeyString(KEY_PREFIX_REGULAR);
111
+ const regularKeyHash = await bcrypt.hash(regularRawKey, 10);
112
+
113
+ // 创建Server Key
114
+ const serverRawKey = this.generateRandomRawKeyString(KEY_PREFIX_SERVER);
115
+ const serverKeyHash = await bcrypt.hash(serverRawKey, 10);
116
+
117
+ const createdAt = new Date().toISOString();
118
+
119
+ try {
120
+ // 开始事务
121
+ this.db.exec('BEGIN TRANSACTION');
122
+
123
+ // 插入Regular Key
124
+ this.db.run(
125
+ 'INSERT INTO api_keys (id, name, description, key_hash, key_prefix, key_type, server_id, created_at, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)',
126
+ [regularKeyId, name, description, regularKeyHash, KEY_PREFIX_REGULAR, KEY_TYPE_REGULAR, serverId, createdAt]
127
+ );
128
+
129
+ // 插入关联的Server Key
130
+ this.db.run(
131
+ 'INSERT INTO api_keys (id, name, description, key_hash, key_prefix, key_type, server_id, regular_key_id, created_at, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)',
132
+ [serverKeyId, `${name} - Server Key`, `Server Key for ${name}`, serverKeyHash, KEY_PREFIX_SERVER, KEY_TYPE_SERVER, serverId, regularKeyId, createdAt]
133
+ );
134
+
135
+ // 提交事务
136
+ this.db.exec('COMMIT');
137
+ this.save();
138
+
139
+ return {
140
+ regularKey: {
141
+ id: regularKeyId,
142
+ rawKey: regularRawKey,
143
+ keyPrefix: KEY_PREFIX_REGULAR,
144
+ keyType: KEY_TYPE_REGULAR,
145
+ serverId
146
+ },
147
+ serverKey: {
148
+ id: serverKeyId,
149
+ rawKey: serverRawKey,
150
+ keyPrefix: KEY_PREFIX_SERVER,
151
+ keyType: KEY_TYPE_SERVER,
152
+ serverId,
153
+ regularKeyId
154
+ }
155
+ };
156
+ } catch (error) {
157
+ this.db.exec('ROLLBACK');
158
+ throw new Error('Could not generate unique API key hash');
159
+ }
160
+ }
161
+
162
+ async createApiKey(name, description = '', keyType = KEY_TYPE_REGULAR, serverId = null, regularKeyId = null) {
163
+ const keyId = uuidv4();
164
+ let prefix;
165
+
166
+ switch(keyType) {
167
+ case KEY_TYPE_ADMIN:
168
+ prefix = KEY_PREFIX_ADMIN;
169
+ break;
170
+ case KEY_TYPE_SERVER:
171
+ prefix = KEY_PREFIX_SERVER;
172
+ break;
173
+ default:
174
+ prefix = KEY_PREFIX_REGULAR;
175
+ }
176
+
177
+ const rawKey = this.generateRandomRawKeyString(prefix);
178
+ const keyHash = await bcrypt.hash(rawKey, 10);
179
+ const createdAt = new Date().toISOString();
180
+
181
+ try {
182
+ this.db.run(
183
+ 'INSERT INTO api_keys (id, name, description, key_hash, key_prefix, key_type, server_id, regular_key_id, created_at, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)',
184
+ [keyId, name, description, keyHash, prefix, keyType, serverId, regularKeyId, createdAt]
185
+ );
186
+ this.save();
187
+ return { keyId, rawKey, keyPrefix: prefix, keyType, serverId, regularKeyId };
188
+ } catch (error) {
189
+ throw new Error('Could not generate unique API key hash');
190
+ }
191
+ }
192
+
193
+ async verifyApiKey(keyToCheck) {
194
+ const stmt = this.db.prepare('SELECT id, key_hash, key_type, server_id, regular_key_id FROM api_keys WHERE is_active = 1');
195
+
196
+ while (stmt.step()) {
197
+ const row = stmt.getAsObject();
198
+ const isValid = await bcrypt.compare(keyToCheck, row.key_hash);
199
+
200
+ if (isValid) {
201
+ stmt.free();
202
+
203
+ this.db.run(
204
+ 'UPDATE api_keys SET last_used = ? WHERE id = ?',
205
+ [new Date().toISOString(), row.id]
206
+ );
207
+ this.save();
208
+
209
+ return {
210
+ id: row.id,
211
+ keyType: row.key_type,
212
+ serverId: row.server_id,
213
+ regularKeyId: row.regular_key_id
214
+ };
215
+ }
216
+ }
217
+
218
+ stmt.free();
219
+ return null;
220
+ }
221
+
222
+ getApiKeyDetailsById(keyId) {
223
+ const stmt = this.db.prepare(
224
+ 'SELECT id, name, description, key_prefix, key_type, server_id, regular_key_id, created_at, last_used, is_active FROM api_keys WHERE id = ?'
225
+ );
226
+ stmt.bind([keyId]);
227
+
228
+ if (stmt.step()) {
229
+ const row = stmt.getAsObject();
230
+ stmt.free();
231
+ return {
232
+ id: row.id,
233
+ name: row.name,
234
+ description: row.description,
235
+ keyPrefix: row.key_prefix,
236
+ keyType: row.key_type,
237
+ serverId: row.server_id,
238
+ regularKeyId: row.regular_key_id,
239
+ createdAt: row.created_at,
240
+ lastUsed: row.last_used,
241
+ isActive: Boolean(row.is_active)
242
+ };
243
+ }
244
+
245
+ stmt.free();
246
+ return null;
247
+ }
248
+
249
+ getAllApiKeysInfo() {
250
+ const stmt = this.db.prepare(
251
+ 'SELECT id, name, description, key_prefix, key_type, server_id, regular_key_id, created_at, last_used, is_active FROM api_keys ORDER BY created_at DESC'
252
+ );
253
+
254
+ const keys = [];
255
+ while (stmt.step()) {
256
+ const row = stmt.getAsObject();
257
+ keys.push({
258
+ id: row.id,
259
+ name: row.name,
260
+ description: row.description,
261
+ keyPrefix: row.key_prefix,
262
+ keyType: row.key_type,
263
+ serverId: row.server_id,
264
+ regularKeyId: row.regular_key_id,
265
+ createdAt: row.created_at,
266
+ lastUsed: row.last_used,
267
+ isActive: Boolean(row.is_active)
268
+ });
269
+ }
270
+
271
+ stmt.free();
272
+ return keys;
273
+ }
274
+
275
+ getServerKeysByRegularKeyId(regularKeyId) {
276
+ const stmt = this.db.prepare(
277
+ 'SELECT id, name, description, key_prefix, key_type, server_id, created_at, last_used, is_active FROM api_keys WHERE regular_key_id = ? AND key_type = ? ORDER BY created_at DESC'
278
+ );
279
+ stmt.bind([regularKeyId, KEY_TYPE_SERVER]);
280
+
281
+ const keys = [];
282
+ while (stmt.step()) {
283
+ const row = stmt.getAsObject();
284
+ keys.push({
285
+ id: row.id,
286
+ name: row.name,
287
+ description: row.description,
288
+ keyPrefix: row.key_prefix,
289
+ keyType: row.key_type,
290
+ serverId: row.server_id,
291
+ createdAt: row.created_at,
292
+ lastUsed: row.last_used,
293
+ isActive: Boolean(row.is_active)
294
+ });
295
+ }
296
+
297
+ stmt.free();
298
+ return keys;
299
+ }
300
+
301
+ toggleApiKeyActivation(keyId, activate) {
302
+ const result = this.db.run(
303
+ 'UPDATE api_keys SET is_active = ? WHERE id = ?',
304
+ [activate ? 1 : 0, keyId]
305
+ );
306
+ this.save();
307
+ return result.changes > 0;
308
+ }
309
+
310
+ deleteApiKeyById(keyId) {
311
+ const keyInfo = this.getApiKeyDetailsById(keyId);
312
+
313
+ if (keyInfo && keyInfo.keyType === KEY_TYPE_ADMIN) {
314
+ const stmt = this.db.prepare(
315
+ 'SELECT COUNT(*) as count FROM api_keys WHERE key_type = ? AND id != ?'
316
+ );
317
+ stmt.bind([KEY_TYPE_ADMIN, keyId]);
318
+ stmt.step();
319
+ const row = stmt.getAsObject();
320
+ stmt.free();
321
+
322
+ if (row.count === 0) {
323
+ throw new Error('Cannot delete the last Admin Key');
324
+ }
325
+ }
326
+
327
+ const result = this.db.run('DELETE FROM api_keys WHERE id = ?', [keyId]);
328
+ this.save();
329
+ return result.changes > 0;
330
+ }
331
+
332
+ logEvent(event, apiKeyId) {
333
+ this.db.run(
334
+ 'INSERT INTO event_logs (event_type, server_name, timestamp, data, api_key_id) VALUES (?, ?, ?, ?, ?)',
335
+ [event.event_type, event.server_name, event.timestamp, JSON.stringify(event.data), apiKeyId]
336
+ );
337
+ this.save();
338
+ }
339
+
340
+ getRecentEvents(limit = 100) {
341
+ const stmt = this.db.prepare(
342
+ 'SELECT * FROM event_logs ORDER BY timestamp DESC LIMIT ?'
343
+ );
344
+ stmt.bind([limit]);
345
+
346
+ const events = [];
347
+ while (stmt.step()) {
348
+ const row = stmt.getAsObject();
349
+ events.push({
350
+ id: row.id,
351
+ eventType: row.event_type,
352
+ serverName: row.server_name,
353
+ timestamp: row.timestamp,
354
+ data: JSON.parse(row.data),
355
+ apiKeyId: row.api_key_id
356
+ });
357
+ }
358
+
359
+ stmt.free();
360
+ return events;
361
+ }
362
+ }
363
+
364
+ module.exports = Database;
365
+ module.exports.KEY_TYPE_ADMIN = KEY_TYPE_ADMIN;
366
+ module.exports.KEY_TYPE_SERVER = KEY_TYPE_SERVER;
367
+ module.exports.KEY_TYPE_REGULAR = KEY_TYPE_REGULAR;
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/events.js ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const router = express.Router();
3
+
4
+ function createEventsRouter(db, manager, requireAnyKey) {
5
+ router.post('/', requireAnyKey, async (req, res) => {
6
+ try {
7
+ const event = req.body;
8
+
9
+ if (!event.event_type || !event.server_name || !event.timestamp || !event.data) {
10
+ return res.status(400).json({ detail: 'Missing required event fields' });
11
+ }
12
+
13
+ db.logEvent(event, req.apiKey.id);
14
+
15
+ const message = {
16
+ type: 'minecraft_event',
17
+ event: event,
18
+ source_key_id_prefix: req.apiKey.id.substring(0, 8)
19
+ };
20
+
21
+ await manager.broadcastToAll(message);
22
+
23
+ res.json({ message: 'Event received and broadcasted' });
24
+ } catch (error) {
25
+ res.status(500).json({ detail: error.message });
26
+ }
27
+ });
28
+
29
+ return router;
30
+ }
31
+
32
+ module.exports = createEventsRouter;
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/health.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const { KEY_TYPE_ADMIN, KEY_TYPE_SERVER, KEY_TYPE_REGULAR } = require('../database');
4
+
5
+ function createHealthRouter(db, manager) {
6
+ router.get('/', (req, res) => {
7
+ try {
8
+ const allKeys = db.getAllApiKeysInfo();
9
+
10
+ res.json({
11
+ status: 'healthy',
12
+ timestamp: new Date().toISOString(),
13
+ active_ws: manager.getActiveConnectionsCount(),
14
+ keys_total: allKeys.length,
15
+ admin_active: allKeys.filter(k => k.keyType === KEY_TYPE_ADMIN && k.isActive).length,
16
+ server_active: allKeys.filter(k => k.keyType === KEY_TYPE_SERVER && k.isActive).length,
17
+ regular_active: allKeys.filter(k => k.keyType === KEY_TYPE_REGULAR && k.isActive).length
18
+ });
19
+ } catch (error) {
20
+ res.status(500).json({ status: 'unhealthy', error: error.message });
21
+ }
22
+ });
23
+
24
+ return router;
25
+ }
26
+
27
+ module.exports = createHealthRouter;
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/keys.js ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const { KEY_TYPE_ADMIN, KEY_TYPE_SERVER, KEY_TYPE_REGULAR } = require('../database');
4
+ const { requireRegularKeyForOwnServerKeys } = require('../auth');
5
+
6
+ function createKeysRouter(db, requireAdminKey) {
7
+ const verifyApiKey = require('../auth').verifyApiKey(db);
8
+ router.post('/', requireAdminKey, async (req, res) => {
9
+ try {
10
+ const { name, description = '', key_type = KEY_TYPE_REGULAR, server_id = null } = req.body;
11
+
12
+ if (!name) {
13
+ return res.status(400).json({ detail: 'Name is required' });
14
+ }
15
+
16
+ if (key_type === KEY_TYPE_REGULAR) {
17
+ // 创建Regular Key并自动生成关联的Server Key
18
+ const result = await db.createRegularKeyWithServerKey(name, description, server_id);
19
+
20
+ res.status(201).json({
21
+ regularKey: {
22
+ ...db.getApiKeyDetailsById(result.regularKey.id),
23
+ key: result.regularKey.rawKey
24
+ },
25
+ serverKey: {
26
+ ...db.getApiKeyDetailsById(result.serverKey.id),
27
+ key: result.serverKey.rawKey
28
+ }
29
+ });
30
+ } else {
31
+ // 创建单个Admin或Server Key
32
+ const result = await db.createApiKey(name, description, key_type, server_id);
33
+ const details = db.getApiKeyDetailsById(result.keyId);
34
+
35
+ res.status(201).json({
36
+ ...details,
37
+ key: result.rawKey
38
+ });
39
+ }
40
+ } catch (error) {
41
+ res.status(500).json({ detail: error.message });
42
+ }
43
+ });
44
+
45
+ router.get('/', requireAdminKey, (req, res) => {
46
+ try {
47
+ const keys = db.getAllApiKeysInfo();
48
+ res.json(keys);
49
+ } catch (error) {
50
+ res.status(500).json({ detail: error.message });
51
+ }
52
+ });
53
+
54
+ // Regular Key获取自己的Server Key列表
55
+ router.get('/server-keys', verifyApiKey(db), requireRegularOrAdminKey, (req, res) => {
56
+ try {
57
+ if (req.apiKey.keyType === KEY_TYPE_ADMIN) {
58
+ // Admin获取所有Server Key
59
+ const allKeys = db.getAllApiKeysInfo();
60
+ const serverKeys = allKeys.filter(key => key.keyType === KEY_TYPE_SERVER);
61
+ res.json(serverKeys);
62
+ } else if (req.apiKey.keyType === KEY_TYPE_REGULAR) {
63
+ // Regular Key获取自己关联的Server Key
64
+ const serverKeys = db.getServerKeysByRegularKeyId(req.apiKey.id);
65
+ res.json(serverKeys);
66
+ }
67
+ } catch (error) {
68
+ res.status(500).json({ detail: error.message });
69
+ }
70
+ });
71
+
72
+ // Regular Key为自己创建新的Server Key
73
+ // 只有Admin Key可以为Regular Key创建Server Key
74
+ router.post('/server-keys', verifyApiKey(db), requireAdminKey, async (req, res) => {
75
+ try {
76
+ const { name, description = '', server_id = null, regular_key_id } = req.body;
77
+
78
+ if (!name) {
79
+ return res.status(400).json({ detail: 'Name is required' });
80
+ }
81
+
82
+ if (!regular_key_id) {
83
+ return res.status(400).json({ detail: 'regular_key_id is required' });
84
+ }
85
+
86
+ const regularKeyInfo = db.getApiKeyDetailsById(regular_key_id);
87
+ if (!regularKeyInfo || regularKeyInfo.keyType !== KEY_TYPE_REGULAR) {
88
+ return res.status(404).json({ detail: 'Invalid Regular Key ID' });
89
+ }
90
+
91
+ const result = await db.createApiKey(name, description, KEY_TYPE_SERVER, server_id, regular_key_id);
92
+ const details = db.getApiKeyDetailsById(result.keyId);
93
+
94
+ res.status(201).json({
95
+ ...details,
96
+ key: result.rawKey
97
+ });
98
+ } catch (error) {
99
+ res.status(500).json({ detail: error.message });
100
+ }
101
+ });
102
+
103
+ router.get('/:key_id', verifyApiKey, requireRegularKeyForOwnServerKeys(db), (req, res) => {
104
+ try {
105
+ const details = db.getApiKeyDetailsById(req.params.key_id);
106
+
107
+ if (!details) {
108
+ return res.status(404).json({ detail: 'API Key not found' });
109
+ }
110
+
111
+ res.json(details);
112
+ } catch (error) {
113
+ res.status(500).json({ detail: error.message });
114
+ }
115
+ });
116
+
117
+ router.patch('/:key_id/activate', verifyApiKey, requireRegularKeyForOwnServerKeys(db), (req, res) => {
118
+ try {
119
+ const keyId = req.params.key_id;
120
+
121
+ if (!db.getApiKeyDetailsById(keyId)) {
122
+ return res.status(404).json({ detail: 'Key not found' });
123
+ }
124
+
125
+ if (db.toggleApiKeyActivation(keyId, true)) {
126
+ return res.json({ message: `Key '${keyId}' activated.` });
127
+ }
128
+
129
+ res.status(500).json({ detail: 'Failed to activate key.' });
130
+ } catch (error) {
131
+ res.status(500).json({ detail: error.message });
132
+ }
133
+ });
134
+
135
+ router.patch('/:key_id/deactivate', verifyApiKey, requireRegularKeyForOwnServerKeys(db), (req, res) => {
136
+ try {
137
+ const keyId = req.params.key_id;
138
+ const keyInfo = db.getApiKeyDetailsById(keyId);
139
+
140
+ if (!keyInfo) {
141
+ return res.status(404).json({ detail: 'Key not found' });
142
+ }
143
+
144
+ // 不能删除自己
145
+ if (keyId === req.apiKey.id) {
146
+ return res.status(400).json({ detail: 'Cannot deactivate your own key.' });
147
+ }
148
+
149
+ // 管理员不能删除最后一个Admin Key
150
+ if (req.apiKey.keyType === KEY_TYPE_ADMIN && keyInfo.keyType === KEY_TYPE_ADMIN) {
151
+ const allKeys = db.getAllApiKeysInfo();
152
+ const activeAdmin = allKeys.filter(k => k.keyType === KEY_TYPE_ADMIN && k.isActive && k.id !== keyId);
153
+
154
+ if (activeAdmin.length === 0) {
155
+ return res.status(400).json({ detail: 'Cannot deactivate last active Admin Key.' });
156
+ }
157
+ }
158
+
159
+ if (db.toggleApiKeyActivation(keyId, false)) {
160
+ return res.json({ message: `Key '${keyId}' deactivated.` });
161
+ }
162
+
163
+ res.status(500).json({ detail: 'Failed to deactivate key.' });
164
+ } catch (error) {
165
+ res.status(500).json({ detail: error.message });
166
+ }
167
+ });
168
+
169
+ router.delete('/:key_id', verifyApiKey, requireRegularKeyForOwnServerKeys(db), (req, res) => {
170
+ try {
171
+ const keyId = req.params.key_id;
172
+
173
+ if (!db.getApiKeyDetailsById(keyId)) {
174
+ return res.status(404).json({ detail: 'Key not found' });
175
+ }
176
+
177
+ // 不能删除自己
178
+ if (keyId === req.apiKey.id) {
179
+ return res.status(400).json({ detail: 'Cannot delete your own key.' });
180
+ }
181
+
182
+ db.deleteApiKeyById(keyId);
183
+ res.status(204).send();
184
+ } catch (error) {
185
+ if (error.message.includes('last Admin Key')) {
186
+ return res.status(400).json({ detail: error.message });
187
+ }
188
+ res.status(500).json({ detail: error.message });
189
+ }
190
+ });
191
+
192
+ return router;
193
+ }
194
+
195
+ module.exports = createKeysRouter;
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/server.js ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const router = express.Router();
3
+
4
+ function createServerRouter(db, manager, requireServerKey) {
5
+ // 获取服务器信息
6
+ router.get('/info', requireServerKey, (req, res) => {
7
+ try {
8
+ // 这里应该从实际的Minecraft服务器获取信息
9
+ // 目前返回模拟数据
10
+ const serverInfo = {
11
+ server_id: req.apiKey.serverId || 'server-001',
12
+ status: 'running',
13
+ online_players: 5,
14
+ max_players: 20,
15
+ version: '1.20.1',
16
+ uptime: '2h 30m',
17
+ tps: 19.8
18
+ };
19
+
20
+ res.json(serverInfo);
21
+ } catch (error) {
22
+ res.status(500).json({ detail: error.message });
23
+ }
24
+ });
25
+
26
+ // 发送服务器命令
27
+ router.post('/command', requireServerKey, (req, res) => {
28
+ try {
29
+ const { command } = req.body;
30
+
31
+ if (!command) {
32
+ return res.status(400).json({ detail: 'Command is required' });
33
+ }
34
+
35
+ // 这里应该向实际的Minecraft服务器发送命令
36
+ // 目前只是记录命令
37
+ console.log(`[Server Command] ${req.apiKey.serverId}: ${command}`);
38
+
39
+ // 广播命令执行事件
40
+ const commandEvent = {
41
+ event_type: 'server_command',
42
+ server_name: req.apiKey.serverId || 'unknown',
43
+ timestamp: new Date().toISOString(),
44
+ data: {
45
+ command: command,
46
+ executed_by: req.apiKey.id,
47
+ server_id: req.apiKey.serverId
48
+ }
49
+ };
50
+
51
+ manager.broadcastToAll({
52
+ type: 'minecraft_event',
53
+ event: commandEvent,
54
+ source_key_id_prefix: req.apiKey.id.substring(0, 8)
55
+ });
56
+
57
+ res.json({
58
+ message: 'Command sent successfully',
59
+ command: command,
60
+ server_id: req.apiKey.serverId
61
+ });
62
+ } catch (error) {
63
+ res.status(500).json({ detail: error.message });
64
+ }
65
+ });
66
+
67
+ return router;
68
+ }
69
+
70
+ module.exports = createServerRouter;
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/server.js ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ require('dotenv').config();
2
+ const express = require('express');
3
+ const path = require('path');
4
+ const { WebSocketServer } = require('ws');
5
+ const http = require('http');
6
+ const Database = require('./database');
7
+ const ConnectionManager = require('./websocket');
8
+ const { verifyApiKey, requireAdminKey, requireServerKey, requireAnyKey } = require('./auth');
9
+ const createKeysRouter = require('./routes/keys');
10
+ const createEventsRouter = require('./routes/events');
11
+ const createHealthRouter = require('./routes/health');
12
+ const createServerRouter = require('./routes/server');
13
+
14
+ const HOST = process.env.SERVER_HOST || '0.0.0.0';
15
+ const PORT = parseInt(process.env.SERVER_PORT || '8000');
16
+
17
+ const app = express();
18
+ const server = http.createServer(app);
19
+ const wss = new WebSocketServer({ noServer: true });
20
+
21
+ const db = new Database(process.env.DATABASE_PATH || 'minecraft_ws.db');
22
+ const manager = new ConnectionManager();
23
+
24
+ app.use(express.json());
25
+
26
+ app.use('/dashboard', express.static(path.join(__dirname, '../dashboard/public')));
27
+
28
+ app.get('/', (req, res) => {
29
+ res.json({
30
+ message: 'Minecraft WebSocket API Server (Node.js)',
31
+ dashboard: '/dashboard',
32
+ websocket: 'ws://' + req.get('host') + '/ws',
33
+ version: '1.0.0'
34
+ });
35
+ });
36
+
37
+ app.use('/manage/keys', verifyApiKey(db), requireAdminKey, createKeysRouter(db, requireAdminKey));
38
+ app.use('/api/events', verifyApiKey(db), requireAnyKey, createEventsRouter(db, manager, requireAnyKey));
39
+ app.use('/health', createHealthRouter(db, manager));
40
+
41
+ server.on('upgrade', async (request, socket, head) => {
42
+ const url = new URL(request.url, `http://${request.headers.host}`);
43
+
44
+ if (url.pathname === '/ws') {
45
+ const apiKey = url.searchParams.get('api_key');
46
+
47
+ if (!apiKey) {
48
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
49
+ socket.destroy();
50
+ return;
51
+ }
52
+
53
+ const result = await db.verifyApiKey(apiKey);
54
+
55
+ if (!result) {
56
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
57
+ socket.destroy();
58
+ return;
59
+ }
60
+
61
+ wss.handleUpgrade(request, socket, head, (ws) => {
62
+ wss.emit('connection', ws, request, result);
63
+ });
64
+ } else {
65
+ socket.destroy();
66
+ }
67
+ });
68
+
69
+ wss.on('connection', (ws, request, apiKeyInfo) => {
70
+ manager.connect(ws, apiKeyInfo.id);
71
+
72
+ ws.on('message', (data) => {
73
+ try {
74
+ const msg = JSON.parse(data.toString());
75
+
76
+ if (msg.type === 'ping') {
77
+ ws.send(JSON.stringify({ type: 'pong' }));
78
+ }
79
+ } catch (error) {
80
+ }
81
+ });
82
+
83
+ ws.on('close', () => {
84
+ manager.disconnect(ws);
85
+ });
86
+
87
+ ws.on('error', () => {
88
+ manager.disconnect(ws);
89
+ });
90
+ });
91
+
92
+ async function startServer() {
93
+ console.log('🚀 启动Minecraft WebSocket API服务器...');
94
+ console.log('='.repeat(50));
95
+
96
+ await db.init();
97
+
98
+ const adminKeyInfo = await db.ensureInitialAdminKey();
99
+
100
+ if (adminKeyInfo) {
101
+ console.log('='.repeat(60));
102
+ console.log('重要: 已生成新的Admin Key!');
103
+ console.log(` 名称: ${adminKeyInfo.name}`);
104
+ console.log(` 密钥: ${adminKeyInfo.key}`);
105
+ console.log('请复制并安全保存此密钥。');
106
+ console.log('您需要使用它来管理API密钥。');
107
+ console.log('如果丢失此密钥且没有其他Admin Key,您可能失去管理员访问权限。');
108
+ console.log('='.repeat(60));
109
+ } else {
110
+ console.log('信息: 已找到现有Admin Key或Admin Key检查已执行。');
111
+ }
112
+
113
+ server.listen(PORT, HOST, () => {
114
+ console.log(`服务器地址: http://${HOST}:${PORT}`);
115
+ console.log(`WebSocket端点: ws://${HOST}:${PORT}/ws`);
116
+ console.log(`健康检查: http://${HOST}:${PORT}/health`);
117
+ console.log('='.repeat(50));
118
+ console.log();
119
+ console.log('💡 首次使用提示:');
120
+ console.log('1. 使用CLI工具创建API密钥:');
121
+ console.log(' node cli/cli.js create-key "MyServer"');
122
+ console.log();
123
+ console.log('2. 生成Minecraft插件配置文件:');
124
+ console.log(' node cli/cli.js generate-config');
125
+ console.log();
126
+ console.log('3. 将API密钥配置到Minecraft插件中');
127
+ console.log('='.repeat(50));
128
+ });
129
+ }
130
+
131
+ startServer().catch(error => {
132
+ console.error('❌ 启动失败:', error);
133
+ process.exit(1);
134
+ });
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/websocket.js ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class ConnectionManager {
2
+ constructor() {
3
+ this.activeConnections = new Map();
4
+ this.connectionKeys = new Map();
5
+ }
6
+
7
+ connect(websocket, apiKeyId) {
8
+ if (!this.activeConnections.has(apiKeyId)) {
9
+ this.activeConnections.set(apiKeyId, new Set());
10
+ }
11
+ this.activeConnections.get(apiKeyId).add(websocket);
12
+ this.connectionKeys.set(websocket, apiKeyId);
13
+ }
14
+
15
+ disconnect(websocket) {
16
+ const apiKeyId = this.connectionKeys.get(websocket);
17
+
18
+ if (apiKeyId && this.activeConnections.has(apiKeyId)) {
19
+ this.activeConnections.get(apiKeyId).delete(websocket);
20
+
21
+ if (this.activeConnections.get(apiKeyId).size === 0) {
22
+ this.activeConnections.delete(apiKeyId);
23
+ }
24
+ }
25
+
26
+ this.connectionKeys.delete(websocket);
27
+ }
28
+
29
+ async broadcastToAll(message) {
30
+ if (this.activeConnections.size === 0) return;
31
+
32
+ const messageStr = JSON.stringify(message);
33
+ const disconnectedWebsockets = [];
34
+
35
+ for (const connectionsSet of this.activeConnections.values()) {
36
+ for (const connection of connectionsSet) {
37
+ try {
38
+ if (connection.readyState === 1) {
39
+ connection.send(messageStr);
40
+ } else {
41
+ disconnectedWebsockets.push(connection);
42
+ }
43
+ } catch (error) {
44
+ disconnectedWebsockets.push(connection);
45
+ }
46
+ }
47
+ }
48
+
49
+ for (const ws of disconnectedWebsockets) {
50
+ this.disconnect(ws);
51
+ }
52
+ }
53
+
54
+ getActiveConnectionsCount() {
55
+ let count = 0;
56
+ for (const connectionsSet of this.activeConnections.values()) {
57
+ count += connectionsSet.size;
58
+ }
59
+ return count;
60
+ }
61
+ }
62
+
63
+ module.exports = ConnectionManager;
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/start-server.bat ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ @echo off
2
+ echo Starting Minecraft WebSocket API Server...
3
+ node src/server.js
hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/start.sh ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ echo "🚀 启动Minecraft WebSocket API服务器..."
4
+ echo "=================================================="
5
+
6
+ if [ ! -d "node_modules" ]; then
7
+ echo "📦 检测到缺少依赖,正在安装..."
8
+ npm install
9
+ fi
10
+
11
+ node src/server.js