Compare commits
No commits in common. "e11814048c5b9a557690fa2d30033742bb54b0f8" and "f58970783f15042d7805e4486e4043cacfd86a46" have entirely different histories.
e11814048c
...
f58970783f
@ -7,23 +7,48 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: |
|
||||||
|
docker build -t awesome-index:${{ github.sha }} .
|
||||||
|
docker tag awesome-index:${{ github.sha }} awesome-index:latest
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
|
needs: build
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy to server
|
||||||
|
uses: appleboy/ssh-action@master
|
||||||
|
with:
|
||||||
|
host: 114.134.187.170
|
||||||
|
username: root
|
||||||
|
password: ${{ secrets.SERVER_PASSWORD }}
|
||||||
|
script: |
|
||||||
|
# 创建部署目录
|
||||||
|
mkdir -p /app/awesome/data /app/awesome/logs
|
||||||
|
|
||||||
- name: Copy files to server
|
- name: Copy files to server
|
||||||
uses: appleboy/scp-action@master
|
uses: appleboy/scp-action@master
|
||||||
with:
|
with:
|
||||||
host: 114.134.187.170
|
host: 114.134.187.170
|
||||||
username: root
|
username: root
|
||||||
password: ${{ secrets.SERVER_PASSWORD }}
|
password: ${{ secrets.SERVER_PASSWORD }}
|
||||||
source: "package.json,package-lock.json,src,public,.env.example,start.sh"
|
source: "docker-compose.yml,start.sh"
|
||||||
target: "/app/awesome/"
|
target: "/app/awesome/"
|
||||||
|
|
||||||
- name: Deploy and start service
|
- name: Load and start container
|
||||||
uses: appleboy/ssh-action@master
|
uses: appleboy/ssh-action@master
|
||||||
with:
|
with:
|
||||||
host: 114.134.187.170
|
host: 114.134.187.170
|
||||||
@ -32,9 +57,14 @@ jobs:
|
|||||||
script: |
|
script: |
|
||||||
cd /app/awesome
|
cd /app/awesome
|
||||||
|
|
||||||
# 如不存在 .env 则从 example 复制
|
# 停止旧容器
|
||||||
[ ! -f .env ] && cp .env.example .env
|
docker compose down --remove-orphans 2>/dev/null || true
|
||||||
|
|
||||||
# 执行启动脚本
|
# 启动新容器
|
||||||
chmod +x start.sh
|
docker compose up -d
|
||||||
bash start.sh
|
|
||||||
|
# 等待启动
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
curl -sf http://localhost:3000/healthz && echo " - 服务已启动" || echo " - 启动失败"
|
||||||
|
|||||||
62
deploy.sh
62
deploy.sh
@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Awesome Index 部署脚本(直接部署,不使用 Docker)
|
# Awesome Index 部署脚本
|
||||||
# 服务器: 114.134.187.170
|
# 服务器: 114.134.187.170
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@ -23,18 +23,64 @@ echo "1. 创建部署目录..."
|
|||||||
sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_HOST" \
|
sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_HOST" \
|
||||||
"mkdir -p $DEPLOY_PATH/data $DEPLOY_PATH/logs"
|
"mkdir -p $DEPLOY_PATH/data $DEPLOY_PATH/logs"
|
||||||
|
|
||||||
echo "2. 上传源码..."
|
echo "2. 上传文件..."
|
||||||
sshpass -p "$REMOTE_PASS" scp -o StrictHostKeyChecking=no -r \
|
sshpass -p "$REMOTE_PASS" scp -o StrictHostKeyChecking=no \
|
||||||
package.json package-lock.json src public .env.example start.sh \
|
docker-compose.yml start.sh .env.example \
|
||||||
"$REMOTE_USER@$REMOTE_HOST:$DEPLOY_PATH/"
|
"$REMOTE_USER@$REMOTE_HOST:$DEPLOY_PATH/"
|
||||||
|
|
||||||
echo "3. 重命名 .env.example 为 .env(如不存在)..."
|
echo "3. 重命名 .env.example 为 .env..."
|
||||||
sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_HOST" \
|
sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_HOST" \
|
||||||
"cd $DEPLOY_PATH && [ ! -f .env ] && cp .env.example .env"
|
"cd $DEPLOY_PATH && [ ! -f .env ] && cp .env.example .env"
|
||||||
|
|
||||||
echo "4. 在服务器上执行启动脚本..."
|
echo "4. 在服务器上构建并启动..."
|
||||||
sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_HOST" \
|
sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_HOST" << 'EOF'
|
||||||
"cd $DEPLOY_PATH && chmod +x start.sh && bash start.sh"
|
cd /app/awesome
|
||||||
|
|
||||||
|
# 创建 Dockerfile 用于服务器构建
|
||||||
|
cat > Dockerfile << 'DOCKERFILE'
|
||||||
|
FROM node:22-bookworm-slim AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci --omit=dev --no-audit --no-fund
|
||||||
|
|
||||||
|
FROM node:22-bookworm-slim
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY package.json ./
|
||||||
|
COPY src ./src
|
||||||
|
COPY public ./public
|
||||||
|
VOLUME ["/app/data"]
|
||||||
|
EXPOSE 3000
|
||||||
|
USER node
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s \
|
||||||
|
CMD node -e "fetch('http://localhost:3000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||||
|
CMD ["node", "src/index.js"]
|
||||||
|
DOCKERFILE
|
||||||
|
|
||||||
|
# 构建镜像
|
||||||
|
docker build -t awesome-index:latest .
|
||||||
|
|
||||||
|
# 停止旧容器
|
||||||
|
docker compose down --remove-orphans 2>/dev/null || true
|
||||||
|
|
||||||
|
# 启动新容器
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
echo "等待服务启动..."
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
if curl -sf http://localhost:3000/healthz > /dev/null 2>&1; then
|
||||||
|
echo "✓ 服务启动成功!"
|
||||||
|
echo " 访问: http://$REMOTE_HOST:3000"
|
||||||
|
echo " 管理: http://$REMOTE_HOST:3000/admin"
|
||||||
|
else
|
||||||
|
echo "✗ 服务启动失败,查看日志:"
|
||||||
|
docker compose logs
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
EOF
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== 部署完成 ==="
|
echo "=== 部署完成 ==="
|
||||||
|
|||||||
@ -1594,32 +1594,6 @@ button.chip:hover {
|
|||||||
.field .input {
|
.field .input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.ai-key-field {
|
|
||||||
position: relative;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.ai-key-field .input {
|
|
||||||
width: 100%;
|
|
||||||
padding-right: 42px;
|
|
||||||
}
|
|
||||||
.eye-toggle {
|
|
||||||
position: absolute;
|
|
||||||
right: 8px;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 4px;
|
|
||||||
border: 0;
|
|
||||||
background: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--ink-dim);
|
|
||||||
}
|
|
||||||
.eye-toggle:hover {
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
.auth-row {
|
.auth-row {
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -139,19 +139,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelectorAll("[data-eye-toggle]").forEach(function (btn) {
|
|
||||||
btn.addEventListener("click", function () {
|
|
||||||
var input = btn.closest("form").querySelector("input[name=key]");
|
|
||||||
var show = input.type === "password";
|
|
||||||
input.type = show ? "text" : "password";
|
|
||||||
btn.setAttribute("aria-label", show ? "隐藏密钥" : "显示密钥");
|
|
||||||
var open = btn.querySelector(".eye-open");
|
|
||||||
var closed = btn.querySelector(".eye-closed");
|
|
||||||
if (open) open.hidden = show;
|
|
||||||
if (closed) closed.hidden = !show;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll("form.source-config").forEach(function (form) {
|
document.querySelectorAll("form.source-config").forEach(function (form) {
|
||||||
form.addEventListener("submit", function (e) {
|
form.addEventListener("submit", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@ -80,7 +80,6 @@ export async function seedTagsAndEntries() {
|
|||||||
|
|
||||||
export async function runMigrate(config) {
|
export async function runMigrate(config) {
|
||||||
await runSchema();
|
await runSchema();
|
||||||
await run(`ALTER TABLE sources ADD COLUMN IF NOT EXISTS api_key VARCHAR`);
|
|
||||||
|
|
||||||
const admin = await findByUsername("admin");
|
const admin = await findByUsername("admin");
|
||||||
if (!admin) {
|
if (!admin) {
|
||||||
|
|||||||
@ -24,7 +24,6 @@ CREATE TABLE IF NOT EXISTS sources (
|
|||||||
adapter VARCHAR,
|
adapter VARCHAR,
|
||||||
cron_expr VARCHAR DEFAULT '0 9 * * 1',
|
cron_expr VARCHAR DEFAULT '0 9 * * 1',
|
||||||
api_key_hash VARCHAR,
|
api_key_hash VARCHAR,
|
||||||
api_key VARCHAR,
|
|
||||||
direct_publish BOOLEAN NOT NULL DEFAULT FALSE,
|
direct_publish BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
last_run_at TIMESTAMPTZ,
|
last_run_at TIMESTAMPTZ,
|
||||||
last_run_status VARCHAR
|
last_run_status VARCHAR
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import * as commentService from "../services/comment.service.js";
|
|||||||
import * as sourceService from "../services/source.service.js";
|
import * as sourceService from "../services/source.service.js";
|
||||||
import * as userService from "../services/user.service.js";
|
import * as userService from "../services/user.service.js";
|
||||||
import { ENTRY_TYPES } from "../services/entry.service.js";
|
import { ENTRY_TYPES } from "../services/entry.service.js";
|
||||||
|
|
||||||
export const pagesRouter = Router();
|
export const pagesRouter = Router();
|
||||||
|
|
||||||
function csv(v) {
|
function csv(v) {
|
||||||
@ -130,51 +131,3 @@ pagesRouter.get("/admin", async (req, res, next) => {
|
|||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
pagesRouter.get("/admin/entries/:id/edit", async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
if (!req.session.userId) return res.redirect("/login");
|
|
||||||
const entry = await entryService.getById(Number(req.params.id));
|
|
||||||
if (!entry) throw Object.assign(new Error("条目不存在"), { status: 404 });
|
|
||||||
const tagTreeAdmin = await tagService.buildTree();
|
|
||||||
res.render("admin-entry-edit", {
|
|
||||||
title: `编辑 · ${entry.title} · Awesome Index`,
|
|
||||||
user: { username: req.session.username, role: req.session.role },
|
|
||||||
isAdmin: req.session.role === "admin",
|
|
||||||
entry,
|
|
||||||
tagTreeAdmin,
|
|
||||||
types: ENTRY_TYPES,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
next(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
pagesRouter.post("/admin/entries/:id/edit", async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
if (!req.session.userId) return res.redirect("/login");
|
|
||||||
const id = Number(req.params.id);
|
|
||||||
const tags = Array.isArray(req.body.tags) ? req.body.tags : req.body.tags ? [req.body.tags] : [];
|
|
||||||
const newTags = String(req.body.new_tags || "")
|
|
||||||
.split(",")
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
const patch = {
|
|
||||||
title: String(req.body.title || "").trim(),
|
|
||||||
type: req.body.type,
|
|
||||||
description_md: String(req.body.description_md || ""),
|
|
||||||
url: String(req.body.url || "").trim(),
|
|
||||||
license: String(req.body.license || "").trim(),
|
|
||||||
stars: Number(req.body.stars) || 0,
|
|
||||||
tags: [...new Set([...tags, ...newTags])],
|
|
||||||
};
|
|
||||||
await entryService.updateEntry(id, patch, { isHumanEditor: true });
|
|
||||||
const status = req.body.status;
|
|
||||||
if (status && entryService.STATUSES.includes(status)) {
|
|
||||||
await entryService.setStatusDirect(id, status);
|
|
||||||
}
|
|
||||||
res.redirect("/admin#sec-content");
|
|
||||||
} catch (e) {
|
|
||||||
next(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|||||||
@ -144,19 +144,6 @@ export async function setStatus(id, status) {
|
|||||||
return getById(id);
|
return getById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setStatusDirect(id, status) {
|
|
||||||
const entry = await get(`SELECT * FROM entries WHERE id = ?`, [id]);
|
|
||||||
if (!entry) throw httpError(404, "条目不存在");
|
|
||||||
if (!STATUSES.includes(status)) throw httpError(400, `未知状态:${status}`);
|
|
||||||
await run(
|
|
||||||
`UPDATE entries SET status = ?, updated_at = now(),
|
|
||||||
verified_at = CASE WHEN ? = 'active' THEN now() ELSE verified_at END
|
|
||||||
WHERE id = ?`,
|
|
||||||
[status, status, id]
|
|
||||||
);
|
|
||||||
return getById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteEntry(id) {
|
export async function deleteEntry(id) {
|
||||||
await run(`DELETE FROM entry_tags WHERE entry_id = ?`, [id]);
|
await run(`DELETE FROM entry_tags WHERE entry_id = ?`, [id]);
|
||||||
await run(`DELETE FROM comments WHERE entry_id = ?`, [id]);
|
await run(`DELETE FROM comments WHERE entry_id = ?`, [id]);
|
||||||
|
|||||||
@ -13,9 +13,9 @@ export async function ensureBuiltins({ aiIngestKey }) {
|
|||||||
const ai = await get(`SELECT id FROM sources WHERE name = 'AI 接口'`);
|
const ai = await get(`SELECT id FROM sources WHERE name = 'AI 接口'`);
|
||||||
if (!ai) {
|
if (!ai) {
|
||||||
await run(
|
await run(
|
||||||
`INSERT INTO sources (kind, name, enabled, api_key_hash, api_key, direct_publish)
|
`INSERT INTO sources (kind, name, enabled, api_key_hash, direct_publish)
|
||||||
VALUES ('ai', 'AI 接口', ?, ?, ?, FALSE)`,
|
VALUES ('ai', 'AI 接口', ?, ?, FALSE)`,
|
||||||
[Boolean(aiIngestKey), keyHash(aiIngestKey), aiIngestKey]
|
[Boolean(aiIngestKey), keyHash(aiIngestKey)]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,10 +68,9 @@ export async function updateSource(id, patch) {
|
|||||||
export async function setAiKey(key) {
|
export async function setAiKey(key) {
|
||||||
const ai = await get(`SELECT id FROM sources WHERE kind = 'ai'`);
|
const ai = await get(`SELECT id FROM sources WHERE kind = 'ai'`);
|
||||||
if (!ai) return;
|
if (!ai) return;
|
||||||
await run(`UPDATE sources SET enabled = ?, api_key_hash = ?, api_key = ? WHERE id = ?`, [
|
await run(`UPDATE sources SET enabled = ?, api_key_hash = ? WHERE id = ?`, [
|
||||||
Boolean(key),
|
Boolean(key),
|
||||||
keyHash(key),
|
keyHash(key),
|
||||||
key,
|
|
||||||
ai.id,
|
ai.id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,90 +0,0 @@
|
|||||||
<%- include('partials/head') %>
|
|
||||||
<%- include('partials/topbar', { active: 'admin' }) %>
|
|
||||||
|
|
||||||
<div class="admin-shell">
|
|
||||||
<nav class="admin-nav" aria-label="管理分区">
|
|
||||||
<h4>管理台 · <%= user.username %></h4>
|
|
||||||
<a href="/admin" style="display:flex;align-items:center;gap:10px;text-decoration:none;color:inherit;border-radius:8px;padding:10px 12px;font-size:14px;font-weight:600;background:var(--ink);color:var(--paper)">← 返回管理台</a>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<main class="admin-main">
|
|
||||||
<p class="anno">// edit · entry #<%= entry.id %> · 保存后即时生效</p>
|
|
||||||
<h1>编辑条目</h1>
|
|
||||||
<p class="admin-desc"><a href="/entries/<%= entry.slug %>" target="_blank">预览:<%= entry.title %></a></p>
|
|
||||||
|
|
||||||
<form id="entry-edit-form" method="post" action="/admin/entries/<%= entry.id %>/edit" style="margin-top:22px;display:grid;gap:18px;max-width:820px">
|
|
||||||
<div class="panel-box">
|
|
||||||
<h3>基础信息</h3>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="field" style="flex:2"><label for="f-title">标题</label>
|
|
||||||
<input class="input" id="f-title" name="title" value="<%= entry.title %>" required /></div>
|
|
||||||
<div class="field" style="flex:1"><label for="f-type">类型</label>
|
|
||||||
<select class="select" id="f-type" name="type" style="width:100%">
|
|
||||||
<% for (const t of types) { %>
|
|
||||||
<option value="<%= t %>" <%= entry.type === t ? 'selected' : '' %>><%= t %></option>
|
|
||||||
<% } %>
|
|
||||||
</select></div>
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="field" style="flex:2"><label for="f-url">链接 URL</label>
|
|
||||||
<input class="input" id="f-url" name="url" type="url" value="<%= entry.url %>" required /></div>
|
|
||||||
<div class="field" style="flex:1"><label for="f-license">License</label>
|
|
||||||
<input class="input" id="f-license" name="license" value="<%= entry.license || '' %>" /></div>
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="field" style="flex:1"><label for="f-stars">Stars</label>
|
|
||||||
<input class="input" id="f-stars" name="stars" type="number" min="0" value="<%= entry.stars || 0 %>" /></div>
|
|
||||||
<div class="field" style="flex:1"><label for="f-status">状态</label>
|
|
||||||
<select class="select" id="f-status" name="status" style="width:100%">
|
|
||||||
<% for (const s of ['active','greyed','pending']) { %>
|
|
||||||
<option value="<%= s %>" <%= entry.status === s ? 'selected' : '' %>><%= s %></option>
|
|
||||||
<% } %>
|
|
||||||
</select></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel-box">
|
|
||||||
<h3>内容描述(Markdown)</h3>
|
|
||||||
<textarea class="textarea" id="f-desc" name="description_md" rows="16"><%= entry.description_md || '' %></textarea>
|
|
||||||
<p style="font-size:12px;color:var(--ink-dim);margin-top:6px;font-family:var(--font-mono)">// 前台展示原文;保存后详情页与搜索都会生效</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel-box">
|
|
||||||
<h3>标签</h3>
|
|
||||||
<p style="font-size:13px;color:var(--ink-dim);margin-bottom:10px">勾选已有的标签;也可以把新标签名填到下方输入框(逗号分隔)</p>
|
|
||||||
<div style="border:1.5px solid var(--ink-faint);border-radius:8px;padding:14px;max-height:340px;overflow:auto">
|
|
||||||
<ul class="tag-tree" style="padding:0;margin:0">
|
|
||||||
<% for (const root of tagTreeAdmin) { %><%- include('partials/tag-check', { node: root, entry }) %><% } %>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="field" style="margin-top:12px"><label for="f-newtags">新增标签</label>
|
|
||||||
<input class="input" id="f-newtags" name="new_tags" placeholder="例如:自托管, Docker 就绪(留空则不改动)" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;gap:10px;align-items:center">
|
|
||||||
<button class="btn btn-primary" type="submit">保存修改</button>
|
|
||||||
<a class="btn" href="/admin#sec-content">取消</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
var form = document.getElementById("entry-edit-form");
|
|
||||||
if (!form) return;
|
|
||||||
form.addEventListener("submit", function (e) {
|
|
||||||
var box = document.querySelector('input[name="tags"]');
|
|
||||||
if (!box) {
|
|
||||||
var hidden = document.createElement("input");
|
|
||||||
hidden.type = "hidden";
|
|
||||||
hidden.name = "tags";
|
|
||||||
hidden.value = "";
|
|
||||||
form.appendChild(hidden);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<%- include('partials/footer-bar') %>
|
|
||||||
@ -37,7 +37,6 @@
|
|||||||
<td class="mono"><%= fmtDate(e.updated_at) %></td>
|
<td class="mono"><%= fmtDate(e.updated_at) %></td>
|
||||||
<td>
|
<td>
|
||||||
<div class="row-actions">
|
<div class="row-actions">
|
||||||
<a class="btn btn-sm" href="/admin/entries/<%= e.id %>/edit">编辑</a>
|
|
||||||
<% if (e.status !== 'active') { %>
|
<% if (e.status !== 'active') { %>
|
||||||
<button class="btn btn-sm" type="button" data-action="entry-status" data-id="<%= e.id %>" data-status="active">上架</button>
|
<button class="btn btn-sm" type="button" data-action="entry-status" data-id="<%= e.id %>" data-status="active">上架</button>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
@ -107,29 +106,25 @@
|
|||||||
<p class="anno">// sources: human + ai + feed · 自动拉取经 ingest/ 采集层进入待核验队列</p>
|
<p class="anno">// sources: human + ai + feed · 自动拉取经 ingest/ 采集层进入待核验队列</p>
|
||||||
<h1>内容源管理</h1>
|
<h1>内容源管理</h1>
|
||||||
|
|
||||||
<% for (const s of sources) { if (s.kind === 'human') continue; %>
|
<% for (const s of sources) { %>
|
||||||
<div class="panel-box" style="margin-top:20px">
|
<div class="panel-box" style="margin-top:20px">
|
||||||
<h3 style="display:flex;align-items:center;gap:10px">
|
<h3 style="display:flex;align-items:center;gap:10px">
|
||||||
<span class="dot <%= s.enabled ? '' : 'dot-off' %>"></span> <%= s.name %>
|
<span class="dot <%= s.enabled ? '' : 'dot-off' %>"></span> <%= s.name %>
|
||||||
<span class="mono" style="font-weight:400;font-size:12px;color:var(--ink-dim)">kind: <%= s.kind %> · 已收录 <%= Number(s.entry_count) %> 条</span>
|
<span class="mono" style="font-weight:400;font-size:12px;color:var(--ink-dim)">kind: <%= s.kind %> · 已收录 <%= Number(s.entry_count) %> 条</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<% if (s.kind === 'ai') { %>
|
<% if (s.kind === 'human') { %>
|
||||||
|
<p style="font-size:13.5px;color:var(--ink-dim)">管理员与编辑在条目接口直接写入,始终可用,改动写入审计日志。</p>
|
||||||
|
<% } else if (s.kind === 'ai') { %>
|
||||||
<div class="switchline">
|
<div class="switchline">
|
||||||
AI 录入接口启用
|
AI 录入接口启用
|
||||||
<span class="mono"><%= s.enabled ? 'ON' : 'OFF' %></span>
|
<span class="mono"><%= s.enabled ? 'ON' : 'OFF' %></span>
|
||||||
</div>
|
</div>
|
||||||
<form id="ai-key-form" style="margin-top:12px">
|
<form id="ai-key-form">
|
||||||
<p style="font-size:13px;color:var(--ink-dim);margin-bottom:6px">设置新的 X-Ingest-Key(留空 = 关闭通道)</p>
|
<p style="font-size:13px;color:var(--ink-dim);margin-bottom:6px">设置新的 X-Ingest-Key(留空 = 关闭通道)</p>
|
||||||
<div style="display:flex;gap:10px;align-items:center;flex-wrap:nowrap">
|
<div style="display:flex;gap:10px;max-width:420px">
|
||||||
<div class="ai-key-field">
|
<input class="input" name="key" type="text" placeholder="sk-…" />
|
||||||
<input class="input" name="key" type="password" value="<%= s.api_key || '' %>" placeholder="sk-…" autocomplete="off" />
|
<button class="btn btn-sm" type="submit">保存密钥</button>
|
||||||
<button type="button" class="eye-toggle" data-eye-toggle aria-label="显示密钥">
|
|
||||||
<svg class="eye-open" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
|
||||||
<svg class="eye-closed" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" hidden><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-sm" type="submit" style="flex:none">保存密钥</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<p class="mono" style="margin-top:12px;font-size:12px;color:var(--ink-dim)">
|
<p class="mono" style="margin-top:12px;font-size:12px;color:var(--ink-dim)">
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
<li>
|
|
||||||
<label class="checkline" style="padding:3px 0">
|
|
||||||
<input type="checkbox" name="tags" value="<%= node.name %>" <%= (entry.tags || []).includes(node.name) ? 'checked' : '' %> />
|
|
||||||
<span><%= node.name %></span>
|
|
||||||
<% if (node.count !== undefined) { %><span class="cnt"><%= node.count %></span><% } %>
|
|
||||||
</label>
|
|
||||||
<% if (node.children && node.children.length) { %>
|
|
||||||
<ul class="tag-tree" style="padding-left:18px;margin:0">
|
|
||||||
<% for (const child of node.children) { %><%- include('tag-check', { node: child, entry }) %><% } %>
|
|
||||||
</ul>
|
|
||||||
<% } %>
|
|
||||||
</li>
|
|
||||||
21
start.sh
21
start.sh
@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Awesome Index 启动脚本(直接部署,不使用 Docker)
|
# Awesome Index 启动脚本
|
||||||
DEPLOY_PATH="$(cd "$(dirname "$0")" && pwd)"
|
DEPLOY_PATH="$(cd "$(dirname "$0")" && pwd)"
|
||||||
cd "$DEPLOY_PATH"
|
cd "$DEPLOY_PATH"
|
||||||
|
|
||||||
@ -11,22 +11,17 @@ if [ ! -f .env ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 安装依赖
|
# 停止旧容器
|
||||||
echo "安装依赖..."
|
echo "停止旧容器..."
|
||||||
npm ci --omit=dev --no-audit --no-fund
|
docker compose down --remove-orphans 2>/dev/null || true
|
||||||
|
|
||||||
# 停止旧进程
|
# 启动新容器
|
||||||
echo "停止旧进程..."
|
|
||||||
pkill -f "node src/index.js" 2>/dev/null || true
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
# 启动服务
|
|
||||||
echo "启动 Awesome Index..."
|
echo "启动 Awesome Index..."
|
||||||
nohup node src/index.js > logs/app.log 2>&1 &
|
docker compose up -d
|
||||||
|
|
||||||
# 健康检查
|
# 健康检查
|
||||||
echo "等待服务就绪..."
|
echo "等待服务就绪..."
|
||||||
sleep 2
|
sleep 5
|
||||||
|
|
||||||
MAX_RETRIES=10
|
MAX_RETRIES=10
|
||||||
RETRY_COUNT=0
|
RETRY_COUNT=0
|
||||||
@ -42,5 +37,5 @@ while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
|
|||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "✗ 服务启动失败,请检查日志: tail -50 logs/app.log"
|
echo "✗ 服务启动失败,请检查日志: docker compose logs"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user