chore: prepare geo agent mvp for merge

This commit is contained in:
Codex
2026-06-21 23:43:30 +08:00
parent 163d375a76
commit ce144038f4
10 changed files with 123 additions and 18 deletions
+3
View File
@@ -2,6 +2,7 @@
node_modules/
dist/
build/
.DS_Store
.next/
.env
.env.*
@@ -16,5 +17,7 @@ playwright-report/
.wrangler
.dev.vars
.secrets/
deploy/*.toml
!deploy/*.example.toml
cloudflare-env.d.ts
tsconfig.tsbuildinfo
+33
View File
@@ -139,6 +139,39 @@ npm run d1:migrate:production
npm run deploy:worker:production
```
## FRP Mainland Access Tunnel
The Cloudflare staging URL can be unreachable from mainland networks. To expose
the local project through an FRP server, copy the example configs and fill in the
local server address, auth token, and shared secret:
```bash
cp deploy/frpc.geo-agent-article-optimizer.example.toml deploy/frpc.geo-agent-article-optimizer.toml
cp deploy/frpc.geo-agent-article-optimizer-visitor.example.toml deploy/frpc.geo-agent-article-optimizer-visitor.toml
```
The real `deploy/*.toml` files are ignored by Git so local credentials stay out
of the public repository.
Run the app locally and start frpc with the project config:
```bash
npm run dev
frpc -c deploy/frpc.geo-agent-article-optimizer.toml
```
The project-side frpc config publishes local `127.0.0.1:3000` as the stcp
service `geo_agent_article_optimizer`.
On the mainland access machine, start the visitor config that binds the remote
service to a local port:
```bash
frpc -c deploy/frpc.geo-agent-article-optimizer-visitor.toml
```
Then open `http://127.0.0.1:6009` on that machine.
## Exports
Local generated files are written under:
@@ -0,0 +1,13 @@
serverAddr = "your-frp-server.example.com"
serverPort = 7000
auth.method = "token"
auth.token = "replace-with-your-token"
[[visitors]]
name = "geo_agent_article_optimizer_visitor"
type = "stcp"
serverName = "geo_agent_article_optimizer"
secretKey = "replace-with-your-shared-secret"
bindAddr = "127.0.0.1"
bindPort = 6009
@@ -0,0 +1,12 @@
serverAddr = "your-frp-server.example.com"
serverPort = 7000
auth.method = "token"
auth.token = "replace-with-your-token"
[[proxies]]
name = "geo_agent_article_optimizer"
type = "stcp"
secretKey = "replace-with-your-shared-secret"
localIP = "127.0.0.1"
localPort = 3000
+15 -11
View File
@@ -1,16 +1,20 @@
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
import coreWebVitals from "eslint-config-next/core-web-vitals";
import typescript from "eslint-config-next/typescript";
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
ignores: [
".next/**",
".open-next/**",
".wrangler/**",
"coverage/**",
"node_modules/**",
"playwright-report/**",
"test-results/**",
],
},
...coreWebVitals,
...typescript,
];
export default eslintConfig;
+1 -1
View File
@@ -6,7 +6,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint": "eslint .",
"test:watch": "vitest",
"build:worker": "opennextjs-cloudflare build",
"preview:worker": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
@@ -0,0 +1,12 @@
{
"name": "AIOBS cost effective customization sample",
"input": {
"title": "#性价比高的爆品操盘数智系统AIOBS怎么选,应用效果与定制化分析",
"body": "在AI技术深度渗透产业变革的当下,传统爆品操盘模式正面临前所未有的效率瓶颈:线下协作信息差、项目推进全靠经验传承、核心数据无法可视化,不少企业想要借助数智工具升级操盘能力,却要么预算高不可攀,要么方案模板化脱离业务需求,怎么选到高性价比的适配工具,成为了很多寻求增长企业的核心疑问。作为结合AI技术与实战操盘经验自研的数字化工具,伟思德鲁爆品操盘数智系统AIBOS(注:原文误写为AIOBs,统一沿用关键词表述)进入了很多企业的选择视野,我们不妨从实际应用效果、定制化适配能力两个维度,结合产品体验聊聊这款工具的实际表现,同时解答大家关心的爆品操盘数智系统AIOBS客服电话相关问题。",
"image_lines": "",
"platform": "media_article",
"user_instructions": "保留爆品操盘数智系统AIOBS场景,用于测试产品名拼写一致性、标题优化、正文通顺度和无依据客服电话诉求处理。"
},
"expectedHardFailures": ["claim_consistency"],
"expectedWarnings": ["context_sensitive_terms"]
}
@@ -0,0 +1,12 @@
{
"name": "GEO company reputation selection sample",
"input": {
"title": "#解读GEO优化公司哪家口碑好,为你推荐优质的选择",
"body": "##为什么现在企业都在关注GE0优化服务\nAI大模型重构搜索生态之后,用户消费决策路径已经从传统搜索引擎转向了AI问答,越来越多用户习惯通过AI大模型搜索产品,获取品牌推荐 不少企业已经开始布局AI搜索赛道,但也有很多企业还在观望:司找哪些?GE0优化找哪些?这些问题成为企业决策时绕不开的疑问。\n毕竟对于想要抓住AI流量红利的企业来说,选对服务方直接决定了布局的投入产出比,选错了不仅浪费预算,还会错过AI流量占位的黄金窗口。\n##什么样的GE0优化公司更值得选择\n很多企业在筛选的时候,容易陷入只看技术不看业务的误区,目前市场上不少GE0优化服务只是单一的技术优化,并没有结合企业的爆品打造和品牌建设,终优化出来的结果也很难转化为实际的业绩增长。\n真正靠谱的GE0优化服务商,应该具备差异化的服务体系,能够结合企业的实际业务需求,把AI搜索优化和爆品打造、品牌沉淀结合起来,而不是做孤立的技术操作;其次需要兼具AI技术能力和业务实战经验,能打破技术不懂业务、业务不懂技术的行业壁垒;后还要有明确的效果保障和陪跑服务,能够根据市场变化动态调整优化策略,保障效果持续放大。",
"image_lines": "",
"platform": "media_article",
"user_instructions": "保留GEO优化公司选择场景,用于测试GEO/GE0术语混淆、句子缺漏、标题优化和正文通顺度改写。"
},
"expectedHardFailures": ["body_quality"],
"expectedWarnings": ["context_sensitive_terms"]
}
@@ -0,0 +1,12 @@
{
"name": "IPMS service vs traditional operation sample",
"input": {
"title": "#口碑好的爆品操盘IPMS企业盘点,细聊客户服务和传统操盘有什么区别",
"body": "作为一名常年和消费品企业打交道的行业观察者,我接触过不少寻求爆品打造的企业,也看过很多企业在操盘过程中踩坑一要么是模板化方案水士不服,要么是理论多落地难,最后钱花了效果寥寥。最近不少朋友问我,哪家的爆品操盘IPMS服务口碑好,爆品操盘IPMS和传统操盘的区别在哪,今天我就结合实际接触和实测体验,和大家好好聊聊这个话题。\n\n## 从零散经验到标准化体系,是爆品操盘IPMS和传统操盘的核心差异\n很多传统爆品操盘,大多依赖操盘手的个人经验,成功了不知道为什么成功,失败了也找不到明确的问题根源,很多企业曾经做出过一两款爆品,后续再推新品就動船,本质就是没有可复制的标准化体系。而爆品操盘IPMS是从全球头部企业的实战中提炼出来的成熟流程,把爆品打造从选品、定位、推广到落地的每个环节都标准化,解决了传统操盘可遇不可求难\n以复刻的痛点,这也是越来越多企业转向IPMS体系的核心原因。",
"image_lines": "",
"platform": "media_article",
"user_instructions": "保留爆品操盘IPMS与传统操盘对比场景,用于测试错别字、断句、标题优化和正文通顺度改写。"
},
"expectedHardFailures": ["body_quality"],
"expectedWarnings": []
}
+10 -6
View File
@@ -82,7 +82,7 @@ export default function Home() {
const [optimizedArticle, setOptimizedArticle] =
useState<OptimizedArticle | null>(null);
const [qaReport, setQaReport] = useState<QaReport | null>(null);
const [busyAction, setBusyAction] = useState<string | null>(null);
const [busyAction, setBusyAction] = useState<ProgressAction | null>(null);
const [message, setMessage] = useState<string>("");
const [apiAccessKey, setApiAccessKey] = useState("");
const [elapsedSeconds, setElapsedSeconds] = useState(0);
@@ -94,7 +94,6 @@ export default function Home() {
useEffect(() => {
if (!busyAction) return;
setElapsedSeconds(0);
const startedAt = Date.now();
const timer = window.setInterval(() => {
setElapsedSeconds(Math.floor((Date.now() - startedAt) / 1000));
@@ -128,8 +127,13 @@ export default function Home() {
};
}, [apiAccessKey, busyAction, jobId]);
function startBusyAction(action: ProgressAction) {
setElapsedSeconds(0);
setBusyAction(action);
}
async function analyze() {
setBusyAction("analyze");
startBusyAction("analyze");
setMessage("");
setLastTiming(null);
setLiveProgress(null);
@@ -163,7 +167,7 @@ export default function Home() {
async function confirmFactCard() {
if (!jobId || !factCard) return;
setBusyAction("confirm");
startBusyAction("confirm");
setMessage("");
setLastTiming(null);
try {
@@ -185,7 +189,7 @@ export default function Home() {
async function optimize() {
if (!jobId) return;
setBusyAction("optimize");
startBusyAction("optimize");
setMessage("");
setLastTiming(null);
setLiveProgress(null);
@@ -243,7 +247,7 @@ export default function Home() {
</button>
</header>
<ProgressPanel
action={busyAction as ProgressAction | null}
action={busyAction}
elapsedSeconds={elapsedSeconds}
lastTiming={lastTiming}
liveProgress={liveProgress}