Spaces:
Sleeping
Sleeping
GitHub Actions
commited on
Commit
·
24dfcef
1
Parent(s):
4bbfbcd
Sync from GitHub (excluding README)
Browse files- .github/scripts/ai_agent.py +48 -79
- hf_repo/.github/workflows/ai-review.yaml +46 -116
- hf_repo/hf_repo/.github/workflows/ai-review.yaml +116 -46
- hf_repo/hf_repo/hf_repo/.github/scripts/ai_agent.py +86 -79
- hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +20 -116
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/scripts/ai_agent.py +140 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +1 -1
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +48 -42
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +71 -60
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +62 -24
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.github/workflows/ai-review.yaml +87 -0
- hf_repo/hf_repo/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
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.gitignore +59 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/Dockerfile +16 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/LICENSE +21 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/cli/cli.js +389 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/app.js +444 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/index.html +103 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/public/style.css +410 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/dashboard/server.js +21 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/docker-compose.yml +14 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/.gitattributes +2 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/package-lock.json +675 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/package.json +31 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/auth.js +83 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/database.js +367 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/events.js +32 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/health.js +27 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/keys.js +195 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/routes/server.js +70 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/server.js +134 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/src/websocket.js +63 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/start-server.bat +3 -0
- hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/hf_repo/start.sh +11 -0
.github/scripts/ai_agent.py
CHANGED
|
@@ -2,9 +2,9 @@ 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"))
|
|
@@ -13,82 +13,51 @@ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_
|
|
| 13 |
event_data = json.loads(os.getenv("EVENT_CONTEXT"))
|
| 14 |
event_name = os.getenv("EVENT_NAME")
|
| 15 |
|
| 16 |
-
# ---
|
| 17 |
-
|
| 18 |
def list_directory(path=".", **kwargs):
|
| 19 |
-
"""列出指定目录下的文件和文件夹"""
|
| 20 |
try:
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 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 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 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 |
-
|
| 61 |
-
|
| 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 |
-
|
| 72 |
-
|
| 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 |
-
# ---
|
| 82 |
-
|
| 83 |
messages = [
|
| 84 |
-
{"role": "system", "content": f"""
|
| 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 |
|
|
@@ -98,40 +67,40 @@ tools = [
|
|
| 98 |
{"type": "function", "function": {"name": "search_keyword", "description": "Search keyword", "parameters": {"type": "object", "properties": {"keyword": {"type": "string"}}}}}
|
| 99 |
]
|
| 100 |
|
| 101 |
-
#
|
| 102 |
-
for
|
| 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 |
-
|
|
|
|
| 111 |
|
| 112 |
if not msg.tool_calls:
|
| 113 |
break
|
| 114 |
|
| 115 |
for tool_call in msg.tool_calls:
|
| 116 |
-
|
| 117 |
-
|
| 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 |
-
#
|
| 131 |
-
|
| 132 |
-
|
|
|
|
| 133 |
|
| 134 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
match = re.search(r'(\{.*?\})', final_res, re.DOTALL)
|
| 136 |
if match:
|
| 137 |
try:
|
|
@@ -141,7 +110,7 @@ if match:
|
|
| 141 |
|
| 142 |
if json_data.get("labels"):
|
| 143 |
issue_obj.add_to_labels(*json_data["labels"])
|
| 144 |
-
if json_data.get("state")
|
| 145 |
issue_obj.edit(state=json_data["state"])
|
| 146 |
|
| 147 |
issue_obj.create_comment(f"### 🤖 AI Agent Execution\n\n{final_res}")
|
|
|
|
| 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"))
|
|
|
|
| 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 |
|
|
|
|
| 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:
|
|
|
|
| 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/workflows/ai-review.yaml
CHANGED
|
@@ -1,116 +1,46 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
event_name
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
with
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 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}")
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf_repo/hf_repo/.github/workflows/ai-review.yaml
CHANGED
|
@@ -1,46 +1,116 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
with:
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/hf_repo/hf_repo/.github/scripts/ai_agent.py
CHANGED
|
@@ -1,106 +1,110 @@
|
|
| 1 |
import os
|
| 2 |
import json
|
| 3 |
-
import
|
| 4 |
from openai import OpenAI
|
| 5 |
-
from github import Github
|
| 6 |
|
| 7 |
-
# 初始化客户端
|
| 8 |
-
|
| 9 |
-
gh = Github(
|
| 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 |
-
# ---
|
| 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]
|
| 29 |
except Exception as e:
|
| 30 |
-
return str(e)
|
| 31 |
|
| 32 |
-
def search_keyword(keyword, path="."):
|
| 33 |
"""在当前目录及其子目录中搜索关键词"""
|
| 34 |
results = []
|
| 35 |
-
|
| 36 |
-
for
|
| 37 |
-
if
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
def get_context():
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
return number, f"PR Author
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
|
| 62 |
if event_name == "issue_comment":
|
| 63 |
actor = event_data["comment"]["user"]["login"]
|
| 64 |
cmd = event_data["comment"]["body"]
|
| 65 |
-
return number, f"
|
| 66 |
|
| 67 |
-
return number,
|
| 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 |
-
|
| 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 |
-
#
|
| 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 |
-
|
| 113 |
-
|
| 114 |
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
if
|
| 136 |
-
issue_obj.add_to_labels(*
|
| 137 |
-
if
|
| 138 |
-
issue_obj.edit(state=
|
| 139 |
-
|
| 140 |
-
issue_obj.create_comment(f"### 🤖 AI Agent
|
|
|
|
| 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/hf_repo/hf_repo/.github/workflows/ai-review.yaml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
name: AI Repository
|
| 2 |
|
| 3 |
on:
|
| 4 |
pull_request:
|
|
@@ -9,11 +9,11 @@ on:
|
|
| 9 |
types: [created]
|
| 10 |
|
| 11 |
jobs:
|
| 12 |
-
ai-
|
| 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 |
-
|
| 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 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 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 |
-
|
| 136 |
-
|
| 137 |
-
body: `### 🤖 AI Assistant Action\n\n${commentBody}`
|
| 138 |
-
});
|
| 139 |
|
| 140 |
-
|
| 141 |
-
|
| 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/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/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:
|
|
|
|
| 1 |
+
name: AI Repository Assistant
|
| 2 |
|
| 3 |
on:
|
| 4 |
pull_request:
|
hf_repo/hf_repo/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 |
-
#
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
git fetch origin ${{ github.event.pull_request.base.ref }}
|
| 36 |
-
git diff origin/${{ github.event.pull_request.base.ref }}...HEAD >
|
|
|
|
| 37 |
else
|
| 38 |
-
|
| 39 |
-
|
| 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 |
-
|
| 49 |
-
echo "number=$
|
|
|
|
| 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 |
-
|
| 72 |
-
|
| 73 |
-
你的任务:分析内容,打上标签,并决定是否关闭或保持开启。
|
| 74 |
-
|
| 75 |
-
仓库现有标签:[${labelNames.join(", ")}]。
|
| 76 |
-
|
| 77 |
-
输出要求:
|
| 78 |
-
必须在回复的第一行输出 JSON 格式的操作指令:
|
| 79 |
-
{"labels": ["label1"], "state": "closed" | "open"}
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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 |
-
//
|
| 118 |
if (config.labels && config.labels.length > 0) {
|
| 119 |
await github.rest.issues.addLabels({ ...issueParams, labels: config.labels });
|
| 120 |
}
|
| 121 |
|
| 122 |
-
//
|
| 123 |
if (config.state) {
|
| 124 |
await github.rest.issues.update({ ...issueParams, state: config.state });
|
| 125 |
}
|
| 126 |
|
| 127 |
-
//
|
| 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/hf_repo/hf_repo/.github/workflows/ai-review.yaml
CHANGED
|
@@ -1,13 +1,20 @@
|
|
| 1 |
-
name: AI
|
| 2 |
|
| 3 |
on:
|
| 4 |
pull_request:
|
| 5 |
-
types: [opened
|
| 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 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
| 29 |
else
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
fi
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
- name: AI
|
| 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('
|
| 48 |
-
if (!content.trim()) return;
|
| 49 |
|
| 50 |
-
// 1.
|
| 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.
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
| 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 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 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 |
-
|
| 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 |
-
|
| 112 |
-
});
|
| 113 |
|
| 114 |
-
//
|
| 115 |
-
if (
|
| 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/hf_repo/hf_repo/.github/workflows/ai-review.yaml
CHANGED
|
@@ -1,10 +1,10 @@
|
|
| 1 |
-
name: AI PR & Issue
|
| 2 |
|
| 3 |
on:
|
| 4 |
pull_request:
|
| 5 |
types: [opened, synchronize]
|
| 6 |
issues:
|
| 7 |
-
types: [opened]
|
| 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
|
|
|
|
| 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 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
const prompt = process.env.EVENT_TYPE === 'PR'
|
| 49 |
-
? `你是一位资深工程师。请审查以下 PR 的代码改动,指出潜在 Bug
|
| 50 |
-
: `你是一位开源项目维护者。请分析以下 Issue
|
| 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.
|
| 63 |
})
|
| 64 |
});
|
| 65 |
|
| 66 |
const data = await response.json();
|
| 67 |
-
const
|
| 68 |
-
const header = process.env.EVENT_TYPE === 'PR' ? "### 🤖 AI Code Review" : "### 🤖 AI Issue Assistant";
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
owner: context.repo.owner,
|
| 80 |
repo: context.repo.repo,
|
| 81 |
-
issue_number:
|
| 82 |
-
|
| 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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
|