Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1014909ac | ||
|
|
0528b1853a | ||
|
|
614f001986 | ||
|
|
28c21522e3 | ||
|
|
c1eb0bbcf3 | ||
|
|
2365bb9e17 |
17
.env.example
Normal file
17
.env.example
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# ========== 与 payjsapi/cnomadcna/nomadvip 同源 ==========
|
||||||
|
NEXT_PUBLIC_POCKETBASE_URL=https://pocketbase.hackrobot.cn
|
||||||
|
PAYMENT_API_URL=http://127.0.0.1:8007
|
||||||
|
PAYMENT_PROVIDER=zpay
|
||||||
|
PAYMENT_DEFAULT_AMOUNT=100
|
||||||
|
NEXT_PUBLIC_SITE_ID=digital
|
||||||
|
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# 与 payjsapi 同源
|
||||||
|
POCKETBASE_EMAIL=xiaoshuang.eric@gmail.com
|
||||||
|
POCKETBASE_PASSWORD=Xiao4669805@
|
||||||
|
|
||||||
|
# ========== 生产环境 ==========
|
||||||
|
# ROOT_DOMAIN=hackrobot.cn
|
||||||
|
# AUTH_COOKIE_DOMAIN=.hackrobot.cn
|
||||||
|
# PAYMENT_API_URL=https://api.hackrobot.cn
|
||||||
|
# NEXT_PUBLIC_SITE_URL=https://digital.hackrobot.cn
|
||||||
11
.env.local
Normal file
11
.env.local
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# 本地调试 - 支付测试 1 元、zpay 渠道
|
||||||
|
NEXT_PUBLIC_POCKETBASE_URL=https://pocketbase.hackrobot.cn
|
||||||
|
PAYMENT_API_URL=http://127.0.0.1:8007
|
||||||
|
PAYMENT_PROVIDER=zpay
|
||||||
|
PAYMENT_DEFAULT_AMOUNT=100
|
||||||
|
NEXT_PUBLIC_SITE_ID=digital
|
||||||
|
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# 与 payjsapi 同源(/api/vip/check 等需要)
|
||||||
|
POCKETBASE_EMAIL=xiaoshuang.eric@gmail.com
|
||||||
|
POCKETBASE_PASSWORD=Xiao4669805@
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -14,8 +14,8 @@
|
|||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
.next/
|
||||||
/out/
|
out/
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
@@ -31,7 +31,7 @@ yarn-error.log*
|
|||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
# .env*
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Link, useTranslation } from "@/i18n/navigation";
|
|||||||
import { VideoPlayer } from "@/app/components/course/VideoPlayer";
|
import { VideoPlayer } from "@/app/components/course/VideoPlayer";
|
||||||
import { LessonMarkdown } from "./LessonMarkdown";
|
import { LessonMarkdown } from "./LessonMarkdown";
|
||||||
import { getLesson } from "@/config/lessons";
|
import { getLesson } from "@/config/lessons";
|
||||||
import { canWatchLesson } from "@/app/lib/course-payment";
|
import { canWatchLesson, isFreeTrial } from "@/app/lib/course-payment";
|
||||||
import { useAuthAndVip } from "@/app/lib/useAuthAndVip";
|
import { useAuthAndVip } from "@/app/lib/useAuthAndVip";
|
||||||
import { getStoredToken, getStoredUser } from "@/app/lib/pocketbase";
|
import { getStoredToken, getStoredUser } from "@/app/lib/pocketbase";
|
||||||
import { setLastWatched, markCompleted } from "@/app/lib/learning";
|
import { setLastWatched, markCompleted } from "@/app/lib/learning";
|
||||||
@@ -31,7 +31,6 @@ export default function LessonPage() {
|
|||||||
|
|
||||||
const lesson = getLesson(moduleIndex, lessonIndex);
|
const lesson = getLesson(moduleIndex, lessonIndex);
|
||||||
const unlocked = canWatchLesson(moduleIndex, lessonIndex, vip);
|
const unlocked = canWatchLesson(moduleIndex, lessonIndex, vip);
|
||||||
const isFreeTrial = moduleIndex === 0 && lessonIndex < 2;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lesson && unlocked) setLastWatched(moduleIndex, lessonIndex, lesson.name);
|
if (lesson && unlocked) setLastWatched(moduleIndex, lessonIndex, lesson.name);
|
||||||
@@ -146,7 +145,7 @@ export default function LessonPage() {
|
|||||||
|
|
||||||
<h1 className="mb-2 flex items-center gap-2 text-xl font-bold sm:text-2xl md:text-3xl">
|
<h1 className="mb-2 flex items-center gap-2 text-xl font-bold sm:text-2xl md:text-3xl">
|
||||||
{lesson.name}
|
{lesson.name}
|
||||||
{isFreeTrial && (
|
{isFreeTrial(moduleIndex, lessonIndex) && (
|
||||||
<span className="rounded bg-[var(--accent)]/20 px-2 py-0.5 text-sm font-normal text-[var(--accent)]">
|
<span className="rounded bg-[var(--accent)]/20 px-2 py-0.5 text-sm font-normal text-[var(--accent)]">
|
||||||
试看
|
试看
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ function resolvePayApiUrl(request: NextRequest): string {
|
|||||||
const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host");
|
const hostHeader = request.headers.get("host") || request.headers.get("x-forwarded-host");
|
||||||
return (
|
return (
|
||||||
(hostHeader ? getApiUrlFromRequest(hostHeader) : null) ||
|
(hostHeader ? getApiUrlFromRequest(hostHeader) : null) ||
|
||||||
process.env.PAYMENT_API_URL ||
|
|
||||||
payConfig.apiUrl
|
payConfig.apiUrl
|
||||||
).replace(/\/$/, "");
|
).replace(/\/$/, "");
|
||||||
}
|
}
|
||||||
@@ -97,7 +96,7 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
const pb = getPocketBaseConfig();
|
const pb = getPocketBaseConfig();
|
||||||
const joinConfig = getJoinPaymentConfig();
|
const joinConfig = getJoinPaymentConfig();
|
||||||
const totalFee = Number(joinConfig?.amount ?? 100);
|
const totalFee = Number(joinConfig?.amount ?? 8800);
|
||||||
|
|
||||||
const checkPayments = await fetch(
|
const checkPayments = await fetch(
|
||||||
`${pb.url}/api/collections/payments/records?filter=${encodeURIComponent(`order_id="${orderId}"`)}&perPage=1`,
|
`${pb.url}/api/collections/payments/records?filter=${encodeURIComponent(`order_id="${orderId}"`)}&perPage=1`,
|
||||||
|
|||||||
@@ -13,11 +13,7 @@ function resolvePayApiUrl(request: NextRequest): string {
|
|||||||
const config = getPaymentConfig();
|
const config = getPaymentConfig();
|
||||||
const hostHeader =
|
const hostHeader =
|
||||||
request.headers.get("host") || request.headers.get("x-forwarded-host");
|
request.headers.get("host") || request.headers.get("x-forwarded-host");
|
||||||
return (
|
return (getApiUrlFromRequest(hostHeader) || config.apiUrl).replace(/\/$/, "");
|
||||||
getApiUrlFromRequest(hostHeader) ||
|
|
||||||
process.env.PAYMENT_API_URL ||
|
|
||||||
config.apiUrl
|
|
||||||
).replace(/\/$/, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPayOrderCookie(response: NextResponse, orderId: string) {
|
function setPayOrderCookie(response: NextResponse, orderId: string) {
|
||||||
@@ -68,36 +64,130 @@ function buildPayHtml(
|
|||||||
function buildQrHtml(
|
function buildQrHtml(
|
||||||
imgSrc: string,
|
imgSrc: string,
|
||||||
returnUrl: string,
|
returnUrl: string,
|
||||||
orderId: string
|
orderId: string,
|
||||||
|
opts?: { channel?: string; successDesc?: string; confirmUrl?: string }
|
||||||
): string {
|
): string {
|
||||||
|
const ch = (opts?.channel || "wxpay").toLowerCase();
|
||||||
|
const isWx = ch === "wxpay" || ch === "wx" || ch === "wechat";
|
||||||
|
const title = isWx ? "微信扫码支付" : "支付宝扫码支付";
|
||||||
|
const hint = isWx ? "请使用微信扫描下方二维码完成支付" : "请使用支付宝扫描下方二维码完成支付";
|
||||||
|
const successDesc = opts?.successDesc || "支付成功,即将跳转";
|
||||||
|
const confirmUrl = opts?.confirmUrl || "/api/pay/confirm";
|
||||||
|
const esc = (s: string) =>
|
||||||
|
s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] || c);
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>扫码支付</title>
|
<title>${esc(title)}</title>
|
||||||
<style>body{font-family:system-ui;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f0f4f8;}h2{color:#333;margin-bottom:8px;}p{color:#64748b;font-size:14px;}img{width:220px;height:220px;margin:20px 0;border:1px solid #e2e8f0;border-radius:8px;background:#fff;}</style></head>
|
<style>
|
||||||
|
*{box-sizing:border-box;}
|
||||||
|
body{font-family:system-ui,-apple-system,sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);}
|
||||||
|
.card{background:#fff;border-radius:20px;padding:40px;box-shadow:0 25px 50px -12px rgba(0,0,0,.25);max-width:420px;}
|
||||||
|
h2{color:#1e293b;margin:0 0 8px;font-size:1.5rem;}
|
||||||
|
.hint{color:#64748b;font-size:14px;margin-bottom:20px;}
|
||||||
|
.wait-row{display:flex;align-items:center;justify-content:center;gap:24px;margin:20px 0;}
|
||||||
|
.qr-wrap img{width:200px;height:200px;border-radius:12px;border:2px solid #e2e8f0;background:#fff;}
|
||||||
|
.countdown-wrap{width:72px;height:72px;flex-shrink:0;position:relative;}
|
||||||
|
.countdown-ring{transform:rotate(-90deg);}
|
||||||
|
.countdown-ring circle{fill:none;stroke-width:5;transition:stroke-dashoffset .5s linear;}
|
||||||
|
.countdown-ring .bg{stroke:#e2e8f0;}
|
||||||
|
.countdown-ring .progress{stroke:#22c55e;stroke-linecap:round;}
|
||||||
|
.countdown-inner{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;}
|
||||||
|
.countdown-num{font-size:1rem;font-weight:700;color:#1e293b;line-height:1;}
|
||||||
|
.countdown-unit{font-size:9px;color:#94a3b8;}
|
||||||
|
.tip{font-size:13px;color:#64748b;margin-top:16px;}
|
||||||
|
.success-wrap{display:none;}
|
||||||
|
.success-wrap.show{display:block;}
|
||||||
|
.wait-wrap.hide{display:none;}
|
||||||
|
.success-icon{font-size:4rem;margin-bottom:16px;animation:scaleIn .4s ease;}
|
||||||
|
.success-title{font-size:1.25rem;color:#16a34a;font-weight:600;margin-bottom:8px;}
|
||||||
|
.success-desc{color:#64748b;font-size:14px;margin-bottom:24px;}
|
||||||
|
.success-countdown{font-size:1.5rem;font-weight:700;color:#667eea;}
|
||||||
|
@keyframes scaleIn{from{transform:scale(0);opacity:0;}to{transform:scale(1);opacity:1;}}
|
||||||
|
@keyframes pulse{0%,100%{opacity:1;}50%{opacity:.7;}}
|
||||||
|
.pulse{animation:pulse 2s ease-in-out infinite;}
|
||||||
|
</style></head>
|
||||||
<body>
|
<body>
|
||||||
<h2>请扫码支付</h2>
|
<div class="card">
|
||||||
<p>支付成功后将自动跳转</p>
|
<div class="wait-wrap" id="waitWrap">
|
||||||
|
<h2>${esc(title)}</h2>
|
||||||
|
<p class="hint">${esc(hint)}</p>
|
||||||
|
<div class="wait-row">
|
||||||
|
<div class="countdown-wrap">
|
||||||
|
<svg class="countdown-ring" width="72" height="72" viewBox="0 0 72 72">
|
||||||
|
<circle class="bg" cx="36" cy="36" r="32"/>
|
||||||
|
<circle class="progress" id="progressCircle" cx="36" cy="36" r="32" stroke-dasharray="201" stroke-dashoffset="0"/>
|
||||||
|
</svg>
|
||||||
|
<div class="countdown-inner">
|
||||||
|
<span class="countdown-num" id="countdownNum">5:00</span>
|
||||||
|
<span class="countdown-unit">分</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="qr-wrap">
|
||||||
<img src="${imgSrc}" alt="支付二维码" />
|
<img src="${imgSrc}" alt="支付二维码" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="tip pulse" id="tip">等待支付中...</p>
|
||||||
|
</div>
|
||||||
|
<div class="success-wrap" id="successWrap">
|
||||||
|
<div class="success-icon">✅</div>
|
||||||
|
<div class="success-title">支付成功</div>
|
||||||
|
<div class="success-desc">${esc(successDesc)}</div>
|
||||||
|
<div class="success-countdown" id="successCountdown">3 秒后跳转</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
(function(){
|
(function(){
|
||||||
try {
|
try{localStorage.setItem("join_pay_order",${JSON.stringify(`${orderId}|${Date.now()}`)});}catch(e){}
|
||||||
localStorage.setItem("join_pay_order", ${JSON.stringify(
|
|
||||||
`${orderId}|${Date.now()}`
|
|
||||||
)});
|
|
||||||
} catch (e) {}
|
|
||||||
var orderId=${JSON.stringify(orderId)};
|
var orderId=${JSON.stringify(orderId)};
|
||||||
var returnUrl=${JSON.stringify(returnUrl)};
|
var returnUrl=${JSON.stringify(returnUrl)};
|
||||||
if(!orderId){return;}
|
if(!orderId){return;}
|
||||||
|
var waitWrap=document.getElementById("waitWrap");
|
||||||
|
var successWrap=document.getElementById("successWrap");
|
||||||
|
var tip=document.getElementById("tip");
|
||||||
|
var countdownNum=document.getElementById("countdownNum");
|
||||||
|
var progressCircle=document.getElementById("progressCircle");
|
||||||
|
var successCountdown=document.getElementById("successCountdown");
|
||||||
|
var maxT=300,left=maxT;
|
||||||
|
var circumference=2*Math.PI*32;
|
||||||
|
function fmt(t){var m=Math.floor(t/60),s=t%60;return m+":"+(s<10?"0":"")+s;}
|
||||||
|
var countdownTimer=setInterval(function(){
|
||||||
|
left--;
|
||||||
|
countdownNum.textContent=left>60?fmt(left):left;
|
||||||
|
progressCircle.style.strokeDashoffset=circumference*(1-left/maxT);
|
||||||
|
if(left<=0){clearInterval(countdownTimer);tip.textContent="支付超时,返回首页";setTimeout(function(){window.location.href="/";},800);}
|
||||||
|
},1000);
|
||||||
|
function showSuccess(){
|
||||||
|
clearInterval(countdownTimer);
|
||||||
|
waitWrap.classList.add("hide");
|
||||||
|
successWrap.classList.add("show");
|
||||||
|
var s=3;
|
||||||
|
var t=setInterval(function(){
|
||||||
|
s--;
|
||||||
|
successCountdown.textContent=s>0?s+" 秒后跳转":"跳转中…";
|
||||||
|
if(s<=0){clearInterval(t);window.location.href=returnUrl;}
|
||||||
|
},1000);
|
||||||
|
}
|
||||||
|
var confirmUrl=${JSON.stringify(confirmUrl)};
|
||||||
|
function doConfirm(attempt){
|
||||||
|
if(attempt>=3){showSuccess();return;}
|
||||||
|
fetch(confirmUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({order_id:orderId})})
|
||||||
|
.then(function(r){return r.json();})
|
||||||
|
.then(function(cd){
|
||||||
|
if(cd&&cd.ok){showSuccess();}
|
||||||
|
else{setTimeout(function(){doConfirm(attempt+1);},800);}
|
||||||
|
})
|
||||||
|
.catch(function(){setTimeout(function(){doConfirm(attempt+1);},800);});
|
||||||
|
}
|
||||||
function check(){
|
function check(){
|
||||||
fetch("/api/pay/status?order_id="+encodeURIComponent(orderId),{cache:"no-store"})
|
fetch("/api/pay/status?order_id="+encodeURIComponent(orderId),{cache:"no-store"})
|
||||||
.then(function(r){return r.json();})
|
.then(function(r){return r.json();})
|
||||||
.then(function(d){
|
.then(function(d){
|
||||||
if(d.ok&&d.paid){window.location.href=returnUrl;return;}
|
if(d.ok&&d.paid){doConfirm(0);return;}
|
||||||
setTimeout(check,1200);
|
setTimeout(check,500);
|
||||||
})
|
})
|
||||||
.catch(function(){setTimeout(check,1200);});
|
.catch(function(){setTimeout(check,500);});
|
||||||
}
|
}
|
||||||
check();
|
setTimeout(check,500);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body></html>`;
|
</body></html>`;
|
||||||
@@ -308,7 +398,10 @@ export async function POST(request: NextRequest) {
|
|||||||
] || c)
|
] || c)
|
||||||
);
|
);
|
||||||
const rawOrderId = String(resData.order_id || created.orderId).trim();
|
const rawOrderId = String(resData.order_id || created.orderId).trim();
|
||||||
const html = buildQrHtml(imgSrc, safeReturnUrl, rawOrderId);
|
const html = buildQrHtml(imgSrc, safeReturnUrl, rawOrderId, {
|
||||||
|
channel: resData.channel || "wxpay",
|
||||||
|
successDesc: "支付成功,课程已解锁",
|
||||||
|
});
|
||||||
const response = new NextResponse(html, {
|
const response = new NextResponse(html, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { "Content-Type": "text/html; charset=utf-8" },
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
||||||
|
|||||||
@@ -10,11 +10,7 @@ function resolvePayApiUrl(request: NextRequest): string {
|
|||||||
const config = getPaymentConfig();
|
const config = getPaymentConfig();
|
||||||
const hostHeader =
|
const hostHeader =
|
||||||
request.headers.get("host") || request.headers.get("x-forwarded-host");
|
request.headers.get("host") || request.headers.get("x-forwarded-host");
|
||||||
return (
|
return (getApiUrlFromRequest(hostHeader) || config.apiUrl).replace(/\/$/, "");
|
||||||
getApiUrlFromRequest(hostHeader) ||
|
|
||||||
process.env.PAYMENT_API_URL ||
|
|
||||||
config.apiUrl
|
|
||||||
).replace(/\/$/, "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { usePayStatusPoll } from "@/app/lib/payment/usePayStatusPoll";
|
|||||||
* 全局支付轮询:支付返回任意页面时检测 paid,调用 confirm 后跳转首页
|
* 全局支付轮询:支付返回任意页面时检测 paid,调用 confirm 后跳转首页
|
||||||
*/
|
*/
|
||||||
export function PayStatusPollProvider() {
|
export function PayStatusPollProvider() {
|
||||||
usePayStatusPoll(() => {
|
usePayStatusPoll((_orderId) => {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState, useEffect } from "react";
|
|||||||
import { Link } from "@/i18n/navigation";
|
import { Link } from "@/i18n/navigation";
|
||||||
import { Section } from "./Section";
|
import { Section } from "./Section";
|
||||||
import { CURRICULUM_CONFIG } from "@/config/course";
|
import { CURRICULUM_CONFIG } from "@/config/course";
|
||||||
import { canWatchLesson } from "@/app/lib/course-payment";
|
import { canWatchLesson, isFreeTrial } from "@/app/lib/course-payment";
|
||||||
import { useAuthAndVip } from "@/app/lib/useAuthAndVip";
|
import { useAuthAndVip } from "@/app/lib/useAuthAndVip";
|
||||||
import { isBookmarked, toggleBookmark, getBookmarks, getCompletedLessons } from "@/app/lib/learning";
|
import { isBookmarked, toggleBookmark, getBookmarks, getCompletedLessons } from "@/app/lib/learning";
|
||||||
|
|
||||||
@@ -99,7 +99,6 @@ export function CurriculumSection() {
|
|||||||
{isOpen && (
|
{isOpen && (
|
||||||
<ul className="border-t border-[var(--border)]">
|
<ul className="border-t border-[var(--border)]">
|
||||||
{c.lessons.map((l) => {
|
{c.lessons.map((l) => {
|
||||||
const free = l.moduleIndex === 0 && l.lessonIndex < 2;
|
|
||||||
const unlocked = canWatchLesson(l.moduleIndex, l.lessonIndex, vip);
|
const unlocked = canWatchLesson(l.moduleIndex, l.lessonIndex, vip);
|
||||||
const key = `${l.moduleIndex}-${l.lessonIndex}`;
|
const key = `${l.moduleIndex}-${l.lessonIndex}`;
|
||||||
const bookmarked = isBookmarked(l.moduleIndex, l.lessonIndex);
|
const bookmarked = isBookmarked(l.moduleIndex, l.lessonIndex);
|
||||||
@@ -126,7 +125,7 @@ export function CurriculumSection() {
|
|||||||
<span className={unlocked ? "text-[var(--foreground)]" : "text-[var(--muted-foreground)]"}>
|
<span className={unlocked ? "text-[var(--foreground)]" : "text-[var(--muted-foreground)]"}>
|
||||||
{l.name}
|
{l.name}
|
||||||
</span>
|
</span>
|
||||||
{free && (
|
{isFreeTrial(l.moduleIndex, l.lessonIndex) && (
|
||||||
<span className="shrink-0 rounded bg-[var(--accent)]/20 px-1.5 py-0.5 text-xs text-[var(--accent)]">
|
<span className="shrink-0 rounded bg-[var(--accent)]/20 px-1.5 py-0.5 text-xs text-[var(--accent)]">
|
||||||
试看
|
试看
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ web电子书也是自己开发的,开源的也很丰富,但比起研究配置,
|
|||||||
|
|
||||||
> SRS直播服务器
|
> SRS直播服务器
|
||||||
|
|
||||||
<!-- 图片已移除 -->
|

|
||||||
|
|
||||||
除了内容直播,还可以结合互动api (弹幕/礼物),进行游戏
|
除了内容直播,还可以结合互动api (弹幕/礼物),进行游戏
|
||||||
|
|
||||||
@@ -421,11 +421,11 @@ ps: 再次申明,`资本/金融`不在本文赘述
|
|||||||
|
|
||||||
> 直接买号/买频道
|
> 直接买号/买频道
|
||||||
|
|
||||||
<!-- 图片已移除 -->
|

|
||||||
|
|
||||||
> 刷粉刷量
|
> 刷粉刷量
|
||||||
|
|
||||||
<!-- 图片已移除 -->
|

|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -4444,3 +4444,458 @@ openclaw甚至可以运行在5$的esp32单片机上,而AI能力,只需要订阅
|
|||||||
即便是云端的大模型如何强大,都推荐在自己的homelab`私有化部署`,不仅仅能保证业务隐私,也可以让算力成为超级杠杆,不必担心token变成巨额账单
|
即便是云端的大模型如何强大,都推荐在自己的homelab`私有化部署`,不仅仅能保证业务隐私,也可以让算力成为超级杠杆,不必担心token变成巨额账单
|
||||||
|
|
||||||
<img src="https://minioweb.hackrobot.cn/hackrobot/download_20260212_094938_23729.jpg" title="" alt="download_20260212_094938_23729.jpg" width="406">
|
<img src="https://minioweb.hackrobot.cn/hackrobot/download_20260212_094938_23729.jpg" title="" alt="download_20260212_094938_23729.jpg" width="406">
|
||||||
|
|
||||||
|
数字游民:低成本AI工作室:云手机,无人直播Tiktok/Youtube/SaaS,赚美金 (28)
|
||||||
|
|
||||||
|
# 数字员工
|
||||||
|
|
||||||
|
<img title="" src="https://minioweb.hackrobot.cn/hackrobot/qweqw_20260306_044520_10932.jpg" alt="qweqw_20260306_044520_10932.jpg" width="358">
|
||||||
|
|
||||||
|
在openclaw(龙虾)出现之前,只能用代码/N8N进行`RPA自动化`,中间的节点,用AI润色
|
||||||
|
|
||||||
|
但是现在,只需要在chatbot (聊天机器人),对它用`大白话`说出要求,就可以实现全部功能了
|
||||||
|
|
||||||
|
比如
|
||||||
|
|
||||||
|
- 分析摄像头流,当有人/车进入画面,进行通知+响铃,并进行门店客流统计,打印报告
|
||||||
|
|
||||||
|
- 浏览器打开`nomadro.com` (出海导航),分析该网站收录了哪些信息,有什么价值
|
||||||
|
|
||||||
|
- 安卓手机打开X/小红书,自动浏览,并分析画面的内容进行数据分析,对特定主题自动点赞/评论
|
||||||
|
|
||||||
|
一行代码都不需要写,直接得到结果
|
||||||
|
|
||||||
|
> 24小时打工
|
||||||
|
|
||||||
|
用GPT进行对话是实时的,它不会在多少小时后给你汇报,但是龙虾可以
|
||||||
|
|
||||||
|
它可以在睡觉的时候,继续工作
|
||||||
|
|
||||||
|
并且每个龙虾,可以设定专业的`角色分工`,比如:客服/程序员/设计师/会计等
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/sdfds_20260306_051002_15775.jpg" title="" alt="sdfds_20260306_051002_15775.jpg" width="387">
|
||||||
|
|
||||||
|
它只是`把网络/电力/算力,变成了AI货币:token`
|
||||||
|
|
||||||
|
可以做员工经营业务,也可以直接对外出售变现
|
||||||
|
|
||||||
|
与雇佣真实的人类比较,它很便宜,且长久
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 硬件
|
||||||
|
|
||||||
|
<img title="" src="https://minioweb.hackrobot.cn/hackrobot/wqqw_20260306_050044_13949.jpg" alt="wqqw_20260306_050044_13949.jpg" width="439">
|
||||||
|
|
||||||
|
开源项目openclaw和大模型对mac芯片/系统做了适配和优化
|
||||||
|
|
||||||
|
所以很多人选择`mac mini`作为家庭服务器
|
||||||
|
|
||||||
|
它静音/省电,能私有化部署,不必担心天价的token账单和隐私风险
|
||||||
|
|
||||||
|
> 但本文以`显卡`GPU为例
|
||||||
|
|
||||||
|
前些年配置了RTX4060TI 16gb和RTX3060TI 12gb,都是NVIDIA,CUDA对AI大模型支持友好
|
||||||
|
|
||||||
|
游戏笔记本也含有8gb的显存
|
||||||
|
|
||||||
|
为了单独配置小龙虾,在海鲜市场,又淘了 RTX3060 12gb,以及` P40 24gb(矿卡)`
|
||||||
|
|
||||||
|
> P40 24gb 矿卡
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/20260306051213_20260306_051229_16255.png" title="" alt="20260306051213_20260306_051229_16255.png" width="344">
|
||||||
|
|
||||||
|
目前市场价0.8k左右,24gb的显存,不用担心服务宕机,效果与RTX 3060 其实差不多,目前在ubuntu server运行`Qwen3.5-35B-A3B`模型,35B占比22gb显存,多模态(图片识别)占用2gb,刚刚好=24gb
|
||||||
|
|
||||||
|
实际速度,`30token/s`
|
||||||
|
|
||||||
|
另外:9B与35B对比,前者是玩具,不足以胜任数字员工的标准
|
||||||
|
|
||||||
|
P40配置如下
|
||||||
|
|
||||||
|
<img src="file:///C:/Users/admin/AppData/Roaming/marktext/images/2026-03-06-05-13-22-image.png" title="" alt="" width="377">
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
<img title="" src="https://minioweb.hackrobot.cn/hackrobot/wqqwd_20260306_051706_17156.jpg" alt="wqqwd_20260306_051706_17156.jpg" width="443">
|
||||||
|
|
||||||
|
大模型选择qwen3.5
|
||||||
|
|
||||||
|
只需要在小黄脸(huggingface)下载`GGUF模型文件`,:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
用`llama.cpp`运行如下命令
|
||||||
|
|
||||||
|
```
|
||||||
|
llama-server -m Qwen3.5-35B-A3B-Q4_K_M.gguf --mmproj mmproj-BF16.gguf -ngl 40 --threads 12 --host 0.0.0.0 --port 8007 -c 32768 --verbose --no-mmap --image-min-tokens 1024 -b 256 -ub 128 -np 1 --main-gpu 0 --reasoning_format none
|
||||||
|
```
|
||||||
|
|
||||||
|
网盘下载地址,可以后台私信: `qwen`,获取网盘地址
|
||||||
|
|
||||||
|
然后就有了是部署在本地的`qwen3.5-35B-A3B`模型的webui界面
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/20260306044856_20260306_045718_13276.png" title="" alt="20260306044856_20260306_045718_13276.png" width="455">
|
||||||
|
|
||||||
|
小龙虾openclaw部署,也只需要一行命令
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -fsSL https://openclaw.ai/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
> openclaw的web管理面板
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/20260306042805_20260306_045918_13672.png" title="" alt="20260306042805_20260306_045918_13672.png" width="458">
|
||||||
|
|
||||||
|
> 对话,配置telegram
|
||||||
|
|
||||||
|
要求获取服务器的配置和信息
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/20260305195953_20260306_045820_13479.png" title="" alt="20260305195953_20260306_045820_13479.png" width="325">
|
||||||
|
|
||||||
|
openclaw交流,可以后台私信:`加群`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最后
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/download_20260306_052519_18769.jpg" title="" alt="download_20260306_052519_18769.jpg" width="344">
|
||||||
|
|
||||||
|
龙虾openclaw用AI控制你的手机/电脑/电器设备
|
||||||
|
|
||||||
|
具体到浏览器网页/app,以及更底层的控制协议http/adb等等
|
||||||
|
|
||||||
|
在不断地迭代升级中,确实能成为一名优秀的数字员工
|
||||||
|
|
||||||
|
它现在并不是玩具,是未来
|
||||||
|
数字游民:低成本AI工作室:云手机,无人直播Tiktok/Youtube/SaaS,赚美金 (29)
|
||||||
|
|
||||||
|
## vibe coding
|
||||||
|
|
||||||
|
在openclaw(龙虾)爆火之前,垂直于编程领域的商业平台`cursor` 和`Claude` 以及`codex`,就已经能非常出色的完成各种任务,比如实现各种网页/app/小程序,全程大白话,不用手写一句代码
|
||||||
|
|
||||||
|
最初用于补充代码段,完成组件,还需要手动编程,但现在已经能`全业务流程,跨项目文件,多线程开发`
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/20260314205131_20260314_205212_20958.png" title="" alt="20260314205131_20260314_205212_20958.png" width="414">
|
||||||
|
|
||||||
|
举个例子,一段话,它就能在`2小时`内生成了3个前端项目,1个共用后端:
|
||||||
|
|
||||||
|
"你是高级架构师,是全栈工程师,也是优秀的设计师,请用nextjs构建`数字游民`为主题的资源导航/数据saas/私域vip的3个网站,并生成基于fastapi的后端项目,使用pocketbase数据库和minio S3存储"
|
||||||
|
|
||||||
|
生成过程,加上样式/排版/功能调教,也不过2小时
|
||||||
|
|
||||||
|
如果外包高级程序员,没有1个月,几个w的预算,是无法实现的,但实际的消费只有20$*3=60美元
|
||||||
|
|
||||||
|
其实也可以使用vscode编辑器加本地大模型(显卡)API,但效果比云端商业模型会逊色一点,适合后期调教,比如qweb35 -35B-A3B目前运行openclaw基本任务很不错
|
||||||
|
|
||||||
|
网站界面/功能如下:
|
||||||
|
|
||||||
|
> 数字游民saas
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/20260314203157_20260314_204949_20488.png" title="" alt="20260314203157_20260314_204949_20488.png" width="206">
|
||||||
|
|
||||||
|
> 资源导航
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/20260314203337_20260314_205019_20589.png" title="" alt="20260314203337_20260314_205019_20589.png" width="446">
|
||||||
|
|
||||||
|
> 私域会员
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/20260314203359_20260314_205042_20664.png" title="" alt="20260314203359_20260314_205042_20664.png" width="461">
|
||||||
|
|
||||||
|
目前网站已经具备了各种功能:
|
||||||
|
|
||||||
|
- markdown文档/电子书/视频播放/网盘下载
|
||||||
|
|
||||||
|
- 登录注册/权限控制/数据可视化/表单
|
||||||
|
|
||||||
|
当然,由于定位比较全面,不是SPA那种landing page
|
||||||
|
|
||||||
|
所以很多具体的模块都要不断实现和优化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 超级助理
|
||||||
|
|
||||||
|
本期只讨论商业平台`cursor `和`Claude `以及`codex`的效果
|
||||||
|
|
||||||
|
openclaw(龙虾)可以查看上一期文章
|
||||||
|
|
||||||
|
除了之前说的vibe coding,氛围编程,普通人要的是偏向于效率或者生产力的任务
|
||||||
|
|
||||||
|
写`思维导图`有很多年了,一个30MB的文件,迭代了无数次的思考和总结
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/20260314205421_20260314_205913_22330.png" title="" alt="20260314205421_20260314_205913_22330.png" width="395">
|
||||||
|
|
||||||
|
让codex对导图文件进行分析总结
|
||||||
|
|
||||||
|
<img title="" src="https://minioweb.hackrobot.cn/hackrobot/mindmap_wechat_long_20260314_211208_24864.png" alt="mindmap_wechat_long_20260314_211208_24864.png" width="428">
|
||||||
|
|
||||||
|
再比如`独角数卡`电商站在服务器异常,去冰箱倒一杯橙汁的功夫,codex就完成了数据备份,服务修复,以及启动脚本优化
|
||||||
|
|
||||||
|
在此之前,我尝试用chatgpt plus对话,复制粘贴,花了半小时都没有搞定docker redis损坏的问题
|
||||||
|
|
||||||
|
模型都是同样的GPT5.4,但是工作流和效果完全不一样
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/20260314211031_20260314_211116_24694.png" title="" alt="20260314211031_20260314_211116_24694.png" width="362">
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 普通人如何利用AI赚钱?
|
||||||
|
|
||||||
|
AI在每个领域可以做的事情实现是太多了
|
||||||
|
|
||||||
|
本文只是简单阐述了vibe coding和文件分析/服务器运维的简单应用
|
||||||
|
|
||||||
|
比如还有视频剪辑/生成,金融量化交易,电商运营等等
|
||||||
|
|
||||||
|
这里有个关键概念是:`平台优先`,不是to c(个人用户),也不是to B (企业)
|
||||||
|
|
||||||
|
如果一个标榜数字游民的人只是靠教别人做数字游民卖课赚钱,那就是击鼓传花的割韭菜
|
||||||
|
|
||||||
|
就像在深圳中心书城看到一本躺在角落"如何编写出版千万的畅销书"的冷笑话~
|
||||||
|
|
||||||
|
海外平台优先是`Youtube/Tiktok/X`,以及独立开发saas( 接入谷歌oauth登录和stripe支付)
|
||||||
|
|
||||||
|
去了解ypp(Youtube)/ Rewards(tiktok) /Premium(X)的广告分成计划,靠流量,美金变现
|
||||||
|
|
||||||
|
再去了解`订阅拉新(offer)/佣金带货(affiliate)/电商卖货(shop)`更多渠道
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/wdw_20260314_215829_1174.jpg" title="" alt="wdw_20260314_215829_1174.jpg" width="436">
|
||||||
|
|
||||||
|
为了更通俗的阐述基本获利方式,接下来以`国内平台`为例子
|
||||||
|
|
||||||
|
> 广告:流量主
|
||||||
|
|
||||||
|
追求长期,可以分享在AI领域的新闻热点,以及实践心得
|
||||||
|
|
||||||
|
- 新模型 / 新 agent / 新浏览器自动化 / 新视频工作流
|
||||||
|
|
||||||
|
- 对创作者更省时间的功能
|
||||||
|
|
||||||
|
- 能把原本 2 小时流程压到 10 分钟的工具
|
||||||
|
|
||||||
|
目前所有媒体平台,都对 hash tag标签 #AI 算法推荐
|
||||||
|
|
||||||
|
短期爆发
|
||||||
|
|
||||||
|
- 写一篇专题,深度好文,用专业的理论和实践心得,使用各种 emoji/gif/表格/视频 表达
|
||||||
|
|
||||||
|
- 推广:付费投流
|
||||||
|
|
||||||
|
基于真实的分享,仅仅平台的广告费就不少,一旦粉丝达到1w以上,商业投放就会纷至沓来
|
||||||
|
|
||||||
|
ps:数据可怜,就打码了,但苍蝇再小也是肉
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/20260314215009_20260314_221543_4554.png" title="" alt="20260314215009_20260314_221543_4554.png" width="257">
|
||||||
|
|
||||||
|
> 拉新:网盘奖励
|
||||||
|
|
||||||
|
网盘的`拉新/转存`,都是有现金激励的
|
||||||
|
|
||||||
|
由于国内网络的限制,以及安装的繁琐,可以为普通用户提供2个东西
|
||||||
|
|
||||||
|
- 大模型下载
|
||||||
|
|
||||||
|
- 一键懒人安装包 (windows)
|
||||||
|
|
||||||
|
没有让用户付费,但每天的被动收入源源不断 (国内是 百度网盘/夸克网盘)
|
||||||
|
|
||||||
|
当然,如同支付一样,官方把渠道授权给了第三方公司,进行管理和分成
|
||||||
|
|
||||||
|
你可以去出海导航 `nomadro.com`,查看第三方平台推荐
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/20260314215058_20260314_221557_4600.png" title="" alt="20260314215058_20260314_221557_4600.png" width="277">
|
||||||
|
|
||||||
|
> 佣金:云服务器
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/download_20260314_220323_2137.jpg" title="" alt="download_20260314_220323_2137.jpg" width="321">
|
||||||
|
|
||||||
|
openclaw的安装,`腾讯云`就提供了云服务器,开箱即用
|
||||||
|
|
||||||
|
在文章里告诉用户,贴上URL/小程序
|
||||||
|
|
||||||
|
企鹅推出了龙虾专属服务器,下单即可使用,大厂出品
|
||||||
|
|
||||||
|
> 远程技术支持
|
||||||
|
|
||||||
|
<img title="" src="https://minioweb.hackrobot.cn/hackrobot/download_20260314_215505_511.jpg" alt="download_20260314_215505_511.jpg" width="274">
|
||||||
|
|
||||||
|
就算写了文章:安装教程,提供一键懒人包和大模型下载,但是依然有很多用户不会
|
||||||
|
|
||||||
|
所以海鲜市场就有非常多人提供:`远程安装`
|
||||||
|
|
||||||
|
只需要利用rustdesk到等远程电脑控制软件,就可以快速解决软件安装问题(你愿意上门也可以)
|
||||||
|
|
||||||
|
ps: 公众号流量主/云服务器佣金/网盘拉新/远程安装
|
||||||
|
|
||||||
|
> 总结
|
||||||
|
|
||||||
|
如果你对AI保持热情,长期分享实践,仅仅国内的微信公众号,就可以有被动收入的基本框架
|
||||||
|
|
||||||
|
当然,更推荐去拓展海外市场,流量和汇率,是国内的10x以上
|
||||||
|
|
||||||
|
在研究挣钱这件事上,先选好利基(比如AI agent),再布置好免费获利框架(ads广告/offer订阅/affiliate佣金),后续再研究更多收费盈利模式(直播/带货/电商/咨询)等等
|
||||||
|
|
||||||
|
先从自己需求出发,再利他
|
||||||
|
|
||||||
|
数字游民:低成本AI工作室:云手机,无人直播Tiktok/Youtube/SaaS,赚美金 (30)
|
||||||
|
|
||||||
|
# AI算力工作室
|
||||||
|
|
||||||
|
关于赚钱,我一直推崇做"无人直播"
|
||||||
|
|
||||||
|
AI的`数字人/一键换脸/姿态迁移`,可以从内容上解决24小时UGC的问题
|
||||||
|
|
||||||
|
小龙虾openclaw也可以从`流程自动化`和`数据分析`上进行管理
|
||||||
|
|
||||||
|
抛开业务本身不谈,这期只简单聊下`本地部署`的自由主义:
|
||||||
|
|
||||||
|
1. Token无限 (电力转成算力)
|
||||||
|
|
||||||
|
2. 隐私保护 (商业数据不被云厂商收集)
|
||||||
|
|
||||||
|
3. 灰色和黑产 (内容开放,无神茶)
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/dqwdq_20260323_041648_5943.jpg" title="" alt="dqwdq_20260323_041648_5943.jpg" width="444">
|
||||||
|
|
||||||
|
比起 `arm开发板(云手机)`,以及`AI眼镜/全景相机`,`显卡`是当下工作室最好的投资
|
||||||
|
|
||||||
|
如果从成本上考虑,可以选择矿卡P40 (24gb)/千元,选择可以含有多个pcie插槽的主板,显存足够
|
||||||
|
|
||||||
|
可以部署多个数字助理,分配到不同的业务
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 大模型:Token自由
|
||||||
|
|
||||||
|
个人不太建议用windows去部署懒人包
|
||||||
|
|
||||||
|
虽然它会让`小白`,快速自部署AI脚本,运行服务,但是由于操作系统本身的特性,会导致性能有所牺牲
|
||||||
|
|
||||||
|
推荐用linux系统,比如ubuntu,来测试AI程序
|
||||||
|
|
||||||
|
由于server服务器,天生就是ssh到终端执行cli操作,遇到任何问题,交给小龙虾或者商业平台(codex)之类,都能自动化快速解决,不必有运维知识
|
||||||
|
|
||||||
|
> 基座:llama.cpp
|
||||||
|
|
||||||
|
qwen3.5开源后,建议下载大模型文件,用`llama.cpp`部署
|
||||||
|
|
||||||
|
这样你就拥有了类似chatgpt一样的网页,以及`标准的api`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> Agent数字助理
|
||||||
|
|
||||||
|
为什么要使用API?
|
||||||
|
|
||||||
|
除了gpt网页对话,现在的`数字助理`,是借助大模型的思考,来实现`agent`操作
|
||||||
|
|
||||||
|
白话就是有`权限和能力`在你的操作系统:windows/mac os/linux/android,执行各种任务
|
||||||
|
|
||||||
|
比如openclaw就可以调用本地的qwen3.5 api,以及telegram实现对话指令 (国内有飞书和微信clawBot插件)
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/20260323040033_20260323_040050_2815.png" title="" alt="20260323040033_20260323_040050_2815.png" width="253">
|
||||||
|
|
||||||
|
> vide coding
|
||||||
|
|
||||||
|
其实就是`AI+独立开发`, 只不过再也不用用手敲打一行代码了
|
||||||
|
|
||||||
|
可以在vscode和cursor集成 本地大模型
|
||||||
|
|
||||||
|
分分钟实现网站功能(登录/支付/工具),然后立马上线 (海外域名/vps,无需备案,秒访问)
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/20260323052413_20260323_052430_19198.png" title="" alt="20260323052413_20260323_052430_19198.png" width="356">
|
||||||
|
|
||||||
|
> 无神茶
|
||||||
|
|
||||||
|
以上的'本地网页gpt'/"Agent数字助理"/'独立开发',都是老生常谈
|
||||||
|
|
||||||
|
但是本地最大的意义就是保护隐私和"无神茶"
|
||||||
|
|
||||||
|
<img title="" src="https://minioweb.hackrobot.cn/hackrobot/20260323041000_20260323_041016_4660.png" alt="20260323041000_20260323_041016_4660.png" width="394">
|
||||||
|
|
||||||
|
阿里公布的模型文件自带"神茶",以至于AI会拒绝回答很多问题
|
||||||
|
|
||||||
|
或者在圣母婊的基础上给出不痛不痒的废话
|
||||||
|
|
||||||
|
但是作为土生土长的神国人,想要生存,赚钱,是不得已需要知道很多灰色/跨境的方法论的
|
||||||
|
|
||||||
|
于是有人基于开源模型,进行训练处理,就得到了解锁的`Uncensored-HauhauCS-Aggressive`版本
|
||||||
|
|
||||||
|
这也是本篇文章的灵魂:`Qwen3.5-35B-A3B-Uncensored-HauhauCS-Aggressive`
|
||||||
|
|
||||||
|
<img title="" src="https://minioweb.hackrobot.cn/hackrobot/dqwd_20260323_042520_7612.jpg" alt="dqwd_20260323_042520_7612.jpg" width="400">
|
||||||
|
|
||||||
|
关于AI的生产力课程:agnet助理,有在筹备
|
||||||
|
|
||||||
|
有兴趣可私信: `加群`
|
||||||
|
|
||||||
|
但出于业务繁忙和后台消息众多,不一定能及时更新上线
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 低成本预算
|
||||||
|
|
||||||
|
虽然一直在讨论`AI的生产力`和`业务的关联`,但回归到硬件,与工作室本身
|
||||||
|
|
||||||
|
如何低成本部署多套设备呢?答案是:`闲鱼`
|
||||||
|
|
||||||
|
<img src="https://minioweb.hackrobot.cn/hackrobot/wqweq_20260323_043903_10299.jpg" title="" alt="wqweq_20260323_043903_10299.jpg" width="409">
|
||||||
|
|
||||||
|
- 1.搜索`电脑/主机 `
|
||||||
|
|
||||||
|
选择`搬家`原因的`同城自提`
|
||||||
|
|
||||||
|
主机比较大,不方便携带,由于要搬家,也比较急出手,这样就可以购买千元以内的主机
|
||||||
|
|
||||||
|
包含完整的CPU芯片/内存/硬盘
|
||||||
|
|
||||||
|
- 2.矿卡/计算卡
|
||||||
|
|
||||||
|
比如大显存的P40 (24GB), 也是千元以下,ps:不推荐魔改卡,容易坏
|
||||||
|
|
||||||
|
当然,如果要省心,也可以选择mac mini,但是一旦会话题吵起来,售价就会水涨船高
|
||||||
|
|
||||||
|
就像是数字游民`旅居地`,最近的惠州,一旦被公开宣扬,势必会造成一波游客新增
|
||||||
|
|
||||||
|
> 成本总计
|
||||||
|
|
||||||
|
按2k/套, 5个数字助理的成本`不过万元`, 且是家电家宽
|
||||||
|
|
||||||
|
在经营业务的投资成本上,不值一提
|
||||||
|
|
||||||
|
无论是内网穿透,还是打通chatbot,都可以随时随地远程使用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最后
|
||||||
|
|
||||||
|
> 没有任何贬义,用着国内的的AI产品,和裸奔有什么区别呢?
|
||||||
|
|
||||||
|
<img title="" src="https://minioweb.hackrobot.cn/hackrobot/20260323045406_20260323_045503_13434.png" alt="20260323045406_20260323_045503_13434.png" width="209">
|
||||||
|
|
||||||
|
除了telegram,其实个人真正的团队和业务,都是打通在开源的`element`,基于matrix通讯协议
|
||||||
|
|
||||||
|
chatbot一直都是最佳的应用入口,网页不是
|
||||||
|
|
||||||
|
有兴趣的可以回顾往期的 `远程团队`
|
||||||
|
|
||||||
|
> 碎碎念
|
||||||
|
|
||||||
|
最近在勘察旅居地,所以即使vibe coding很多专题网站,但内容也没来得及更新
|
||||||
|
|
||||||
|
周末针对`小红书受众`在loft民宿做了2场沙龙 (入口在`nomadro.com`)
|
||||||
|
|
||||||
|
也是后期的一个方向:`旅居/共创`,而不是纯粹的`技术/业务`单向输出分享
|
||||||
|
|
||||||
|
ps: 相对于咖啡馆/茶舍/联合办公空间/户外天幕 `200/h的场地报价`,loft民宿性价比很高,配上投影仪,更易于交流分享
|
||||||
|
|
||||||
|
数字游民系列至此写了1-30篇,其中序号缺失的是被平台删除了,最近大动作不断,有些如履薄冰
|
||||||
|
|
||||||
|
可后台私信:`电子书`,下载完整pdf文档
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
* 与 join 报名支付分离,课程解锁用 VIP 状态
|
* 与 join 报名支付分离,课程解锁用 VIP 状态
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** 前 2 章为试看,无需支付 */
|
/** 前 2 章试看(暂时关闭,全部需 VIP 解锁) */
|
||||||
export function isFreeTrial(moduleIndex: number, lessonIndex: number): boolean {
|
export function isFreeTrial(_moduleIndex: number, _lessonIndex: number): boolean {
|
||||||
if (moduleIndex === 0 && lessonIndex < 2) return true;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,18 +54,27 @@ function parsePendingOrder(raw: string | null): [string | null, boolean] {
|
|||||||
return [orderId, true];
|
return [orderId, true];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePayStatusPoll(onPaid: () => void): boolean {
|
export function usePayStatusPoll(onPaid: (orderId: string) => void): boolean {
|
||||||
const [polling, setPolling] = useState(false);
|
const [polling, setPolling] = useState(false);
|
||||||
const onPaidRef = useRef(onPaid);
|
const onPaidRef = useRef(onPaid);
|
||||||
onPaidRef.current = onPaid;
|
onPaidRef.current = onPaid;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const urlOrderId =
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? new URLSearchParams(window.location.search).get("order_id") || ""
|
||||||
|
: "";
|
||||||
const cookieRaw = getCookie(COOKIE_NAME);
|
const cookieRaw = getCookie(COOKIE_NAME);
|
||||||
const [cookieOrderId, cookieValid] = parsePendingOrder(cookieRaw);
|
const [cookieOrderId, cookieValid] = parsePendingOrder(cookieRaw);
|
||||||
const storageRaw = getStorage(STORAGE_NAME);
|
const storageRaw = getStorage(STORAGE_NAME);
|
||||||
const [storageOrderId, storageValid] = parsePendingOrder(storageRaw);
|
const [storageOrderId, storageValid] = parsePendingOrder(storageRaw);
|
||||||
const orderId = cookieValid ? cookieOrderId : storageValid ? storageOrderId : null;
|
let orderId = cookieValid ? cookieOrderId : storageValid ? storageOrderId : null;
|
||||||
const activeRaw = cookieValid ? cookieRaw : storageValid ? storageRaw : null;
|
let activeRaw = cookieValid ? cookieRaw : storageValid ? storageRaw : null;
|
||||||
|
if (!orderId && urlOrderId.trim()) {
|
||||||
|
orderId = urlOrderId.trim();
|
||||||
|
activeRaw = `${orderId}|${Date.now()}`;
|
||||||
|
persistPendingOrder(activeRaw);
|
||||||
|
}
|
||||||
|
|
||||||
if (!orderId) {
|
if (!orderId) {
|
||||||
if (cookieRaw || storageRaw) {
|
if (cookieRaw || storageRaw) {
|
||||||
@@ -134,7 +143,7 @@ export function usePayStatusPoll(onPaid: () => void): boolean {
|
|||||||
} catch {
|
} catch {
|
||||||
/* 忽略,不影响跳转 */
|
/* 忽略,不影响跳转 */
|
||||||
}
|
}
|
||||||
onPaidRef.current();
|
onPaidRef.current(orderId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export const FAQ_CONFIG = {
|
|||||||
title: "常见问题",
|
title: "常见问题",
|
||||||
items: [
|
items: [
|
||||||
{ q: "课程可以永久回看吗?", a: "可以,报名后永久有效,随时回看。" },
|
{ q: "课程可以永久回看吗?", a: "可以,报名后永久有效,随时回看。" },
|
||||||
{ q: "适合零基础吗?", a: "适合。课程从入门到进阶,前 2 节可免费试看。" },
|
{ q: "适合零基础吗?", a: "适合。课程从入门到进阶;视频试看暂时关闭,报名后可解锁全部章节。" },
|
||||||
{ q: "有社群答疑吗?", a: "有,报名后加入专属社群,导师和同学一起交流。" },
|
{ q: "有社群答疑吗?", a: "有,报名后加入专属社群,导师和同学一起交流。" },
|
||||||
{ q: "支持退款吗?", a: "7 天内不满意可申请全额退款。" },
|
{ q: "支持退款吗?", a: "7 天内不满意可申请全额退款。" },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -16,11 +16,20 @@ const isDev = process.env.NODE_ENV === "development";
|
|||||||
export const ROOT_DOMAIN =
|
export const ROOT_DOMAIN =
|
||||||
process.env.ROOT_DOMAIN || process.env.NEXT_PUBLIC_ROOT_DOMAIN || "hackrobot.cn";
|
process.env.ROOT_DOMAIN || process.env.NEXT_PUBLIC_ROOT_DOMAIN || "hackrobot.cn";
|
||||||
|
|
||||||
/** 支付 API 默认地址(本地可用 PAYMENT_API_URL 覆盖,如 http://192.168.41.222:8007) */
|
/** 支付 API 默认地址。生产环境固定为 api.${ROOT_DOMAIN},避免 .env 中 127.0.0.1 导致支付失败 */
|
||||||
export const DEFAULT_PAYMENT_API_URL = isDev
|
export const DEFAULT_PAYMENT_API_URL = isDev
|
||||||
? "http://127.0.0.1:8007"
|
? "http://127.0.0.1:8007"
|
||||||
: `https://api.${ROOT_DOMAIN}`;
|
: `https://api.${ROOT_DOMAIN}`;
|
||||||
|
|
||||||
|
/** 生产环境若 PAYMENT_API_URL 指向 localhost,则忽略并使用 api 域名 */
|
||||||
|
export function resolvePaymentApiUrl(): string {
|
||||||
|
const url = process.env.PAYMENT_API_URL || DEFAULT_PAYMENT_API_URL;
|
||||||
|
if (!isDev && /^(https?:\/\/)?(127\.0\.0\.1|localhost)(:\d+)?(\/|$)/i.test(url)) {
|
||||||
|
return `https://api.${ROOT_DOMAIN}`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
/** PocketBase 默认地址 */
|
/** PocketBase 默认地址 */
|
||||||
export const DEFAULT_POCKETBASE_URL = `https://pocketbase.${ROOT_DOMAIN}`;
|
export const DEFAULT_POCKETBASE_URL = `https://pocketbase.${ROOT_DOMAIN}`;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import { getThemeConfig } from "./site.config";
|
import { getThemeConfig } from "./site.config";
|
||||||
import {
|
import {
|
||||||
DEFAULT_PAYMENT_API_URL,
|
DEFAULT_PAYMENT_API_URL,
|
||||||
|
resolvePaymentApiUrl,
|
||||||
DEFAULT_POCKETBASE_URL,
|
DEFAULT_POCKETBASE_URL,
|
||||||
DEFAULT_MINIO_HOST,
|
DEFAULT_MINIO_HOST,
|
||||||
} from "./domain.config";
|
} from "./domain.config";
|
||||||
@@ -74,7 +75,7 @@ export const VIP_MASTER_SITE_ID = "vip";
|
|||||||
|
|
||||||
export function getApiUrlFromRequest(hostHeader: string | null): string | null {
|
export function getApiUrlFromRequest(hostHeader: string | null): string | null {
|
||||||
if (!hostHeader) return null;
|
if (!hostHeader) return null;
|
||||||
if (!isDev) return process.env.PAYMENT_API_URL || DEFAULT_PAYMENT_API_URL;
|
if (!isDev) return resolvePaymentApiUrl();
|
||||||
const hostname = hostHeader.split(":")[0];
|
const hostname = hostHeader.split(":")[0];
|
||||||
return `http://${hostname}:${PAYJSAPI_PORT}`;
|
return `http://${hostname}:${PAYJSAPI_PORT}`;
|
||||||
}
|
}
|
||||||
@@ -97,9 +98,9 @@ export function getPocketBaseConfig(themeOverride?: { joinCollection?: string })
|
|||||||
export function getPaymentConfig(themeOverride?: { amount?: number; type?: string }): PaymentConfig {
|
export function getPaymentConfig(themeOverride?: { amount?: number; type?: string }): PaymentConfig {
|
||||||
const base = {
|
const base = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
apiUrl: process.env.PAYMENT_API_URL || DEFAULT_PAYMENT_API_URL,
|
apiUrl: resolvePaymentApiUrl(),
|
||||||
provider: (process.env.PAYMENT_PROVIDER as "zpay" | "xorpay") || "zpay",
|
provider: (process.env.PAYMENT_PROVIDER as "zpay" | "xorpay") || "zpay",
|
||||||
defaultAmount: Number(process.env.PAYMENT_DEFAULT_AMOUNT) || 100,
|
defaultAmount: Number(process.env.PAYMENT_DEFAULT_AMOUNT) || 8800,
|
||||||
defaultType: process.env.PAYMENT_DEFAULT_TYPE || "meetup",
|
defaultType: process.env.PAYMENT_DEFAULT_TYPE || "meetup",
|
||||||
zpaySubmitUrl: process.env.ZPAY_SUBMIT_URL || "https://zpayz.cn/submit.php",
|
zpaySubmitUrl: process.env.ZPAY_SUBMIT_URL || "https://zpayz.cn/submit.php",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export const THEME_CONFIGS: Record<ThemeId, ThemeConfig> = {
|
|||||||
accent: "amber",
|
accent: "amber",
|
||||||
},
|
},
|
||||||
services: {
|
services: {
|
||||||
join: { amount: 100, type: "meetup" },
|
join: { amount: 8800, type: "meetup" },
|
||||||
pocketbaseJoinCollection: "solan",
|
pocketbaseJoinCollection: "solan",
|
||||||
shopUrl: process.env.NEXT_PUBLIC_SHOP_URL || "https://store.hackrobot.cn/", // 独立站域名
|
shopUrl: process.env.NEXT_PUBLIC_SHOP_URL || "https://store.hackrobot.cn/", // 独立站域名
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,22 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
/** 由 scripts/build-safe.cjs 设置;在 VPS 上也可手动 export 后执行 pnpm build */
|
||||||
|
const lowMemBuild = process.env.NEXT_BUILD_LOW_MEM === "1";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
experimental: {
|
experimental: {
|
||||||
serverActions: { bodySizeLimit: "25mb" },
|
serverActions: { bodySizeLimit: "25mb" },
|
||||||
|
...(lowMemBuild
|
||||||
|
? {
|
||||||
|
// 限制并行与预渲染并发,降低峰值内存(Next 16 默认 Turbopack 构建)
|
||||||
|
cpus: 1,
|
||||||
|
turbopackMemoryLimit: 1536 * 1024 * 1024,
|
||||||
|
staticGenerationMaxConcurrency: 1,
|
||||||
|
webpackMemoryOptimizations: true,
|
||||||
|
parallelServerCompiles: false,
|
||||||
|
parallelServerBuildTraces: false,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
async headers() {
|
async headers() {
|
||||||
const noCacheHeaders = [
|
const noCacheHeaders = [
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3000 -H 0.0.0.0",
|
"dev": "next dev -p 3000 -H 0.0.0.0",
|
||||||
"build": "next build",
|
"build": "node ./scripts/build-safe.cjs",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
|
|||||||
30
scripts/build-safe.cjs
Normal file
30
scripts/build-safe.cjs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 低端 VPS / 小内存环境:限制 Node 堆、开启 Next 低内存构建选项。
|
||||||
|
* 用法:pnpm build(默认经本脚本)
|
||||||
|
* 可选环境变量:
|
||||||
|
* NEXT_BUILD_HEAP_MB — Node --max-old-space-size(默认 1536)
|
||||||
|
*/
|
||||||
|
const { spawnSync } = require("child_process");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const heapMb = process.env.NEXT_BUILD_HEAP_MB || "1536";
|
||||||
|
const nextBin = path.join(
|
||||||
|
path.dirname(require.resolve("next/package.json")),
|
||||||
|
"dist",
|
||||||
|
"bin",
|
||||||
|
"next"
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = spawnSync(
|
||||||
|
process.execPath,
|
||||||
|
["--max-old-space-size=" + heapMb, nextBin, "build"],
|
||||||
|
{
|
||||||
|
stdio: "inherit",
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
NEXT_BUILD_LOW_MEM: "1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(result.status ?? 1);
|
||||||
Reference in New Issue
Block a user